Multiple NavigationLinks leading to UIViewControllerRepresentable destination ends up with blank screen - swiftui

I have found a minimal SwiftUI app that exhibits a bug:
class HelloWorldVC: UIViewController {
override func loadView() {
super.loadView()
let label = UILabel()
label.text = "Hello, World"
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
label.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
}
struct ViewControllerContainer: UIViewControllerRepresentable {
let vc: UIViewController
func makeUIViewController(context: Context) -> some UIViewController { vc }
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
struct ContentView: View {
var body: some View {
NavigationView {
// Works:
//NavigationLink("View UIKit VC", destination: ViewControllerContainer(vc: HelloWorldVC()))
// Only loads the UIKit view once. It's blank on subsequent tries.
NavigationLink(
"Detail Screen",
destination: NavigationLink(
"View UIKit VC",
destination: ViewControllerContainer(vc: HelloWorldVC())
)
)
}
}
}
Steps to reproduce:
Tap "Detail Screen"
Tap "View UIKit VC". You will see the "Hello, World" UIViewController.
Tap Back
Tap "View UIKit VC"
Expected:
You should see the "Hello, World" UIViewController again
Actual:
You will see a blank view. This will happen as many times as you try.
Note: In the commented out code, it works properly if you only have one layer deep of NavigationLink.

You are not working with UIViewControllerRepresentable correctly. You need to create a new view controller inside makeUIViewController, reusing it breaks the view controller lifecycle in your case.
The UIViewControllerRepresentable properties can be passed to the view controller when you create or update it, as follows:
struct ViewControllerContainer: UIViewControllerRepresentable {
let props: Int
func makeUIViewController(context: Context) -> some UIViewController {
HelloWorldVC(props: props)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
uiViewController.props = props
}
}

Related

How to make UIViewControllerRepresentable ViewController reusable in many SwiftUI views?

I have managed to create a UIKit UIAlertController and present it to my SwiftUI View. I had to create a UIKit UIAlertController so i can be able to change the colors of the cancel button and submit button as well as make the submit button bold. It worked but now i need to use the UIAlertController in other SwiftUI Views so i have to make it reusable.
How can i make UIViewControllerRepresentable ViewController reusable in many SwiftUI views without having to copy and past the code below? I am thinking of using a class and just instantiate the class and use the UIKit UIAlertController. Any help or small example appreciated.
This is the UIViewControllerRepresentable struct in my SwiftUI view. The code below has to be reusable without having to add it in each SwiftUI view.
struct UIAlertViewPopup: UIViewControllerRepresentable {
#Binding var show: Bool
let viewModel: SettingsViewModel
var title: String
var message: String
func makeUIViewController(context: UIViewControllerRepresentableContext<UIAlertViewPopup>) -> some UIViewController {
return UIViewController()
}
//update UIKit from SwiftUI
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<UIAlertViewPopup>) {
if (self.show) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .destructive) { (action) in
self.show = false
}
let submitAction = UIAlertAction(title: "Edit profile", style: .default) { (action) in
viewModel(perform: .editProfile)
self.show = false
}
submitAction.setValue(UIColor.accentBlue, forKey: "titleTextColor")
cancelAction.setValue(UIColor.accentPrimary, forKey: "titleTextColor")
alert.addAction(cancelAction)
alert.addAction(submitAction)
alert.preferredAction = submitAction
DispatchQueue.main.async {
uiViewController.present(alert, animated: true, completion: {
self.show = false
})
}
}
}
//update SwiftUI from UIKit through delegates
func makeCoordinator() -> UIAlertViewPopup.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIAlertViewDelegate {
var control: UIAlertViewPopup
init(_ control: UIAlertViewPopup) {
self.control = control
}
}
}
I use it in my SwiftUI view like this
#State var isAlertVisible: Bool = false
Button {
isAlertVisible = true
}
then on the .background modifier
.background(UIAlertViewPopup(show: $viewModel.isAlertVisible, viewModel: viewModel, title: "", message: "hello from UIKit alert"))

Remove title when pushing EKCalendarChooser to Navigation Stack with SwiftUI

I'm working on an app where I want to push the EKCalendarChooser View Controller to the navigation stack with a navigation link. Everything works as expected apart from the fact that I can't get rid of some magic title/label.
I want to hide the title marked with the red rectangle in the image.
I'm using the following code to push the view:
NavigationLink(destination: CalendarChooser(eventStore: self.eventStore)
.edgesIgnoringSafeArea([.top,.bottom])
.navigationTitle("My Navigation Title")) {
Text("Calendar Selection")
}
And this is my UIViewControllerRepresentable
import SwiftUI
import EventKitUI
struct CalendarChooser: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
#Environment(\.presentationMode) var presentationMode
let eventStore: EKEventStore
func makeUIViewController(context: UIViewControllerRepresentableContext<CalendarChooser>) -> UINavigationController {
let chooser = EKCalendarChooser(selectionStyle: .multiple, displayStyle: .allCalendars, entityType: .event, eventStore: eventStore)
chooser.selectedCalendars = Set(eventStore.selectableCalendarsFromSettings)
chooser.delegate = context.coordinator
chooser.showsDoneButton = false
chooser.showsCancelButton = false
return UINavigationController(rootViewController: chooser)
}
func updateUIViewController(_ uiViewController: UINavigationController, context: UIViewControllerRepresentableContext<CalendarChooser>) {
}
class Coordinator: NSObject, UINavigationControllerDelegate, EKCalendarChooserDelegate {
var parent: CalendarChooser
init(_ parent: CalendarChooser) {
self.parent = parent
}
func calendarChooserDidFinish(_ calendarChooser: EKCalendarChooser) {
let selectedCalendarIDs = calendarChooser.selectedCalendars.compactMap { $0.calendarIdentifier }
UserDefaults.savedCalendarIDs = selectedCalendarIDs
NotificationCenter.default.post(name: .calendarSelectionDidChange, object: nil)
parent.presentationMode.wrappedValue.dismiss()
}
func calendarChooserDidCancel(_ calendarChooser: EKCalendarChooser) {
parent.presentationMode.wrappedValue.dismiss()
}
}
}
Note that I'm not even sure that I'm on the right track here and I'm open for any solution.
I think I've found a solution to my own problem. With a small modification
to my UIViewControllerRepresentable the view looks the way I want it to. More specifically to the updateUIViewController function:
func updateUIViewController(_ uiViewController: UINavigationController, context: UIViewControllerRepresentableContext<CalendarChooser>) {
uiViewController.setNavigationBarHidden(true, animated: false) // This line!
}
By doing this I keep the navigation controls and title from the navigation link, which looks like this:

How to present Storyboard ViewController SwiftUI?

I have a modal sheet in which half of the screen, pervious was shown to the user by present(), I want to see the same effect from swift UI
struct StripePaymentViewNav: UIViewControllerRepresentable {
typealias UIViewControllerType = StripePaymentWithPromoCodeViewController
func makeUIViewController(context: Context) -> StripePaymentWithPromoCodeViewController {
let storyboard = UIStoryboard(storyboard: .main)
let stripePaymentWithPromoCodeView = storyboard.instantiateViewController(withIdentifier: "StripePaymentWithPromoCodeView") as! StripePaymentWithPromoCodeViewController
stripePaymentWithPromoCodeView.modalPresentationStyle = .overFullScreen
return stripePaymentWithPromoCodeView
}
func updateUIViewController(_ uiViewController: StripePaymentWithPromoCodeViewController, context: Context) {
}
}
.sheet(isPresented: $viewModel.state.navigateToStripePayment, content: {
StripePaymentViewNav()
})
this worked however, half of the screen is the modal sheep and the other half is white

Navigate from one embedded UIViewController in swiftUI to another embedded UIViewcontroller in swift UI

I have a UIViewController which is embedded in swiftUI View
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(overlayShow: OverlayShow())
}
}
struct OnboardingWrapper: UIViewControllerRepresentable {
var overlayShow: OverlayShow
func makeUIViewController(context: Context) -> ReturnSomthing {
let controller = ReturnSomthing(shareConfig: onboardingConfig)
controller.showOverlay = overlayShow
return controller
}
func updateUIViewController(_ uiViewController: ReturnSomthing, context: Context) {
}
public typealias UIViewControllerType = ReturnSomthing
}
I want to be able to Navigate directly from SwiftUI to another SwiftUI view with embedded UIViewController
SO, I did on a view of SwiftUI
struct ContentView: View {
#ObservedObject var overlayShow: OverlayShow = OverlayShow()
var body: some View {
NavigationView {
NavigationLink(destination: OverlayView(overlay: LoadingUnknownOverlayViewContainer(description: "it is spinning :0", designModel: LoadingOverlayDesignModel(type: .unknown))), isActive: $overlayShow.isShowingOverlay) {
OnboardingWrapper(overlayShow: overlayShow)
}
}
}
}
the condition for navigation to be active is the overlayShow.isShowingOverlay is true. this variable is controlled in UIViewController with an ObservableObject.
the problem is that no matter the value of overlayShow.isShowingOverlay wherever I tap the first UIViewController which is OnboardingWrapper the navigation is activated and goes to the next page.
To be more clear I post a screen recording.
Please how can I solve this problem.
It sounds like you are seeing OnboardingWrapper and when you click on it, you are seeing OverlayView.
Perhaps this would help:
var body: some View {
NavigationView {
OnboardingWrapper(overlayShow: overlayShow)
NavigationLink(destination: OverlayView(
overlay:LoadingUnknownOverlayViewContainer(
description: "it is spinning :0",
designModel: LoadingOverlayDesignModel(type: .unknown))),
isActive: $overlayShow.isShowingOverlay) {
EmptyView()
}
}
}
OnboardingWrapper will appear, then when overlayShow.isShowingOverlay is true, the OverlayView should be presented.

Make UIKit ContextMenu preview provider frame fit a SwiftUI view inside a UIHostingControler

I am learning Swift & SwiftUI as a hobby with no UIKit background, so I am not sure if this is currently possible. I would really like to use UIKit's context menus with SwiftUI (e.g. to implement submenus, action attributes and maybe custom preview providers).
My original idea was to create a LegacyContextMenuView with UIViewControllerRepresentable. Then I'd use a UIHostingController to add a SwiftUI View as a child of a UIViewController ContainerViewController to which I'd add a UIContextMenuInteraction.
My current solution kinda works but when the context menu is activated the preview frame of the 'ContainerViewController' view does not fit the size of UIHostingController view. I am not familiar with UIKit's layout system so I'd like to know:
Is it possible to add such constrains while the preview is activated?
Is it possible to preserve the clipShape of the underlying SwiftUI view inside the preview provider?
The code:
// MARK: - Describes a UIKit Context Menu
struct LegacyContextMenu {
let title: String
let actions: [UIAction]
var actionProvider: UIContextMenuActionProvider {
{ _ in
UIMenu(title: title, children: actions)
}
}
init(actions: [UIAction], title: String = "") {
self.actions = actions
self.title = title
}
}
// MARK: - A View that brings UIKit context menus into the SwiftUI world
struct LegacyContextMenuView<Content: View>: UIViewControllerRepresentable {
let content: Content
let menu: LegacyContextMenu
func makeUIViewController(context: Context) -> UIViewController {
let controller = ContainerViewController(rootView: content)
let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator)
controller.view.addInteraction(menuInteraction)
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
class Coordinator: NSObject, UIContextMenuInteractionDelegate {
let parent: LegacyContextMenuView
init(parent: LegacyContextMenuView) { self.parent = parent }
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration?
{
// previewProvider nil = using the default UIViewController: ContainerViewController
UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: parent.menu.actionProvider)
}
}
class ContainerViewController: UIViewController {
let hostingController: UIHostingController<Content>
init(rootView: Content) {
self.hostingController = UIHostingController(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
setupHostingController()
setupContraints()
// Additional setup required?
}
func setupHostingController() {
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
}
// Not familiar with UIKit's layout system so unsure if this is the best approach
func setupContraints() {
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints([
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostingController.view.leftAnchor.constraint(equalTo: view.leftAnchor),
hostingController.view.rightAnchor.constraint(equalTo: view.rightAnchor)
])
}
}
}
// MARK: - Simulate SwiftUI syntax
extension View {
func contextMenu(_ legacyContextMenu: LegacyContextMenu) -> some View {
self.modifier(LegacyContextViewModifier(menu: legacyContextMenu))
}
}
struct LegacyContextViewModifier: ViewModifier {
let menu: LegacyContextMenu
func body(content: Content) -> some View {
LegacyContextMenuView(content: content, menu: menu)
}
}
Then to test, I use this:
// MARK - A sample view with custom content shape and a dynamic frame
struct SampleView: View {
#State private var isLarge = false
let viewClipShape = RoundedRectangle(cornerRadius: 50.0)
var body: some View {
ZStack {
Color.blue
Text(isLarge ? "Large" : "Small")
.foregroundColor(.white)
.font(.largeTitle)
}
.onTapGesture { isLarge.toggle() }
.clipShape(viewClipShape)
.contentShape(viewClipShape)
.frame(height: isLarge ? 250 : 150)
.animation(.easeInOut, value: isLarge)
}
}
struct ContentView: View {
var body: some View {
SampleView()
.contextMenu(LegacyContextMenu(actions: [sampleAction], title: "My Menu"))
.padding(.horizontal)
}
let sampleAction = UIAction(
title: "Remove",
image: UIImage(systemName: "trash.fill"),
identifier: nil,
attributes: UIMenuElement.Attributes.destructive,
handler: { _ in print("Pressed 'Remove'") })
}
While long pressing, the context menu scaling animation respects the content shape of SampleView for both small and large sizes, but the preview pops out like this:
you must set preferredContentSize of ViewController to fit content size u want