I have a UIViewController that is used in SwiftUI by a UIViewControllerRepresentable.
When I navigate back from the controller to the SwiftUI view, deinit is not called (as I would expect).
However, when I perform a new navigation to the UIViewControllerRepresentable, deinit is called first (for the previous controller)
Is there something I'm missing here with UIViewControllerRepresentable, why doesn't it get deallocated?
import SwiftUI
import UIKit
class MyViewController: UIViewController {
deinit() {
print("This isn't called until the second initiation of the UIViewControllerRepresentable")
}
}
struct MyViewControllerWrapper: UIViewControllerRepresentable {
typealias UIViewControllerType = MyViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<MyViewControllerWrapper>) -> MyViewControllerWrapper.UIViewControllerType {
let vc = UIViewControllerType(requestType: requestType)
return vc
}
func updateUIViewController(_ uiViewController: MyViewControllerWrapper.UIViewControllerType, context: UIViewControllerRepresentableContext<MyViewControllerWrapper>) {
}
}
Related
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:
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
}
}
i tried to make a view like bellow in SwiftUi without any success Customized info window swift ui
Since this question doesn't have too much detail, I will be going off of some assumptions. First, I am assuming that you are calling the MapView through a UIViewControllerRepresentable.
I am not too familiar with the Google Maps SDK, but this is possible through the GMSMapViewDelegate Methods. After implementing the proper GMSMapViewDelegate method, you can use ZStacks to present the image that you would like to show.
For example:
struct MapView: UIViewControllerRepresentable {
var parentView: ContentView
func makeUIViewController(context: Context) {
let mapView = GMSMapView()
return mapView
}
func updateUIViewController(_ uiViewController: GMSMapView, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, GMSMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
//Use the proper Google Maps Delegate method to find out if a marker was tapped and then show the image by doing: parent.parentView.isShowingInformationImage = true.
}
}
In your SwiftUI view that you would like to put this MapView in, you can do the following:
struct ContentView: View {
#State var isShowingInformationImage = false
var body: some View {
ZStack {
if isShowingInformationImage {
//Call the View containing the image
}
MapView(parentView: self)
}
}
}
I'm looking to call a function inside a UIKit UIViewController from a button managed by Swift UI
In my Swift UI View I have:
struct CameraView: View {
var body: some View {
cameraViewController = CameraViewController()
...
which I see creates two instances, one directly created just like calling any class, and the other created by the required makeUIViewController method needed for Swift UI to manage UIKit UIViewControllers.
However when I attached a function to a button in my Swift UI say, cameraViewController.takePhoto() The instance that is referenced is not the one displayed.
How can I obtain the specific instance that is displayed?
There are probably multiple solutions to this problem, but one way or another, you'll need to find a way to keep a reference to or communicate with the UIViewController. Because SwiftUI views themselves are pretty transient, you can't just store a reference in the view itself, because it could get recreated at any time.
Tools to use:
ObservableObject -- this will let you store data in a class instead of a struct and will make it easier to store references, connect data, etc
Coordinator -- in a UIViewRepresentable, you can use a Coordinator pattern which will allow you to store references to the UIViewController and communicate with it
Combine Publishers -- these are totally optional, but I've chosen to use them here since they're an easy way to move data around without too much boilerplate code.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var vcLink = VCLink()
var body: some View {
VStack {
VCRepresented(vcLink: vcLink)
Button("Take photo") {
vcLink.takePhoto()
}
}
}
}
enum LinkAction {
case takePhoto
}
class VCLink : ObservableObject {
#Published var action : LinkAction?
func takePhoto() {
action = .takePhoto
}
}
class CustomVC : UIViewController {
func action(_ action : LinkAction) {
print("\(action)")
}
}
struct VCRepresented : UIViewControllerRepresentable {
var vcLink : VCLink
class Coordinator {
var vcLink : VCLink? {
didSet {
cancelable = vcLink?.$action.sink(receiveValue: { (action) in
guard let action = action else {
return
}
self.viewController?.action(action)
})
}
}
var viewController : CustomVC?
private var cancelable : AnyCancellable?
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIViewController(context: Context) -> CustomVC {
return CustomVC()
}
func updateUIViewController(_ uiViewController: CustomVC, context: Context) {
context.coordinator.viewController = uiViewController
context.coordinator.vcLink = vcLink
}
}
What happens here:
VCLink is an ObservableObject that I'm using as a go-between to communicate between views
The ContentView has a reference to the VCLink -- when the button is pressed, the Publisher on VCLink communicates that to any subscribers
When the VCRepresented is created/updated, I store a reference to the ViewController and the VCLink in its Coordinator
The Coordinator takes the Publisher and in its sink method, performs an action on the stored ViewController. In this demo, I'm just printing the action. In your example, you'd want to trigger the photo itself.
It's possible to make a reference from SwiftUI to your view controller if you need to call its functions directly and without unnecessary code:
class Reference<T: AnyObject> {
weak var object: T?
}
class PlayerViewController: UIViewController {
func resume() {
print("resume")
}
func pause() {
print("pause")
}
}
struct PlayerView: UIViewControllerRepresentable {
let reference: Reference<PlayerViewController>
func makeUIViewController(context: Context) -> PlayerViewController {
let controller = PlayerViewController()
// Set controller to the reference
reference.object = controller
return controller
}
func updateUIViewController(_ viewController: PlayerViewController, context: Context) {
}
}
struct ContentView: View {
let reference = Reference<PlayerViewController>()
var body: some View {
Button("Resume") {
reference.object?.resume()
}
Button("Pause") {
reference.object?.pause()
}
PlayerView(reference: reference)
}
}
I have made a custom UIViewController called ViewControllerA and want to be able to use it so I made a UIViewControllerRepresentable called ViewControllerARepresentable as shown below, the problem though is that when I call ViewControllerARepresentable in my SwiftUI view and pass a value for stringToUpdateTextView, the ViewControllerA says the htmlTextView(UITextView) in ViewControllerA is nil and I'm not sure why.
ViewControllerARepresentable(stringToUpdateTextView: "<html>Send some Html Text as string here</html>")
ViewControllerARepresentable
public struct ViewControllerARepresentable: UIViewControllerRepresentable {
var stringToUpdateTextView: String
public func makeUIViewController(context: Context) -> ViewControllerA {
let viewcontrollerA = ViewControllerA(testString: testingString)
return viewcontrollerA
}
public func updateUIViewController(_ uiViewController: ViewControllerA, context: Context) {}
}
ViewControllerA
open class ViewControllerA: UIViewController {
public var stringToUpdateTextView: String
override open func viewDidLoad() {
super.viewDidLoad()
htmlTextView.text = stringToUpdateTextView
}
#IBOutlet weak var htmlTextView: UITextView!
public init(testString: String) {
self.testString = testString
super.init(nibName: nil, bundle: nil)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Crash occurs at htmlTextView.text = stringToUpdateTextView saying that htmlTextView.text is nil even though its an IBOutlet.
Any Change made to the htmlTextView like background color ,etc, also causes a crash if called in viewDidAppear or viewDidLoad
When instantiating your view controller in makeUIViewController, the outlets haven't been initialised yet.
The following code loads your view controller from the storyboard, and updates the properties in updateUIViewController:
ViewController.swift
import UIKit
import SwiftUI
class ViewController: UIViewController {
#IBOutlet weak var htmlTextView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
struct ViewControllerWrapper: UIViewControllerRepresentable {
typealias UIViewControllerType = ViewController
#Binding var text: String
func makeUIViewController(context: Context) -> ViewController {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
guard let viewController = storyboard.instantiateViewController(
identifier: "ViewController") as? ViewController else {
fatalError("Cannot load from storyboard")
}
return viewController
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
uiViewController.htmlTextView.text = text
}
}
struct ViewControllerPreview: PreviewProvider {
static var previews: some View {
ViewControllerWrapper(text: .constant("hello world!"))
}
}
SwiftUIView.swift
struct SwiftUIView: View {
#State var text = "Text"
var body: some View {
HStack {
TextField("Text:", text: $text)
ViewControllerWrapper(text: $text)
}
}
}