UIRepresentable from external class... is that possible? - swiftui

I am dealing with UIRepresentable.
I have a part like
struct MyRepresentable: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var control: MyScrollView
 
init(_ control: MyScrollView) {
self.control = control
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// User is currently scrolling
}
#objc func handleRefresh(sender: UIRefreshControl) {
sender.endRefreshing()
}
}
The problem is that what must go inside this coordinator is monstrous, a lot of delegate functions.
I am trying to pass this to a separate file, to reduce the clutter on the representable file.
Then I created a new file like this
class MyExternalCoordinator:NSObject, MyDelegate {
// here goes the monstrous code
}
and then, inside my UIViewRepresentable class
func makeCoordinator() -> Coordinator {
MyExternalCoordinator()
}
I get the message
Cannot convert return expression of type 'MyExternalCoordinator' to return type 'MyRepresentable.Coordinator'
any ideas?

Make return type aligned, like
func makeCoordinator() -> MyExternalCoordinator {
MyExternalCoordinator()
}

Related

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:

Calling functions from UIViewController in SwiftUI

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)
}
}

UITextField not updating to change in ObservableObject (SwiftUI)

I am trying to make a UITextView that edits a value currentDisplayedAddress. The value is also changed by other views, and I want the UITextView to update its text when that occurs.
The view initializes correctly, and I can edit currentDisplayedAddress from AddressTextField with no problem and trigger relevant view updates. However, when the value is changed by other views, the textField's text does not change, even though textField.text prints the correct updated value inside updateUIView and other views update accordingly.
I have no idea what may have caused this. Any help is extremely appreciated.
struct AddressTextField: UIViewRepresentable {
private let textField = UITextField(frame: .zero)
var commit: () -> Void
#EnvironmentObject var userData: UserDataModel
func makeUIView(context: UIViewRepresentableContext<AddressTextField>) -> UITextField {
textField.text = self.userData.currentDisplayedAddress
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<AddressTextField>) {
if self.textField.text != self.userData.currentDisplayedAddress {
DispatchQueue.main.async {
self.textField.text = self.userData.currentDisplayedAddress
}
}
}
(...)
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, UITextFieldDelegate {
var addressTextField: AddressTextField
func textFieldShouldReturn(_ textField: UITextField) -> Bool { //delegate method
textField.resignFirstResponder()
addressTextField.userData.currentDisplayedAddress = textField.text ?? String()
addressTextField.commit()
return true
}
}
}
You should not create UITextField as property, because AddressTextField struct can be recreated during parent update. The makeUIView is exactly the place to create UI-instances, the representable is handling its reference on your behalf.
So here is fixed variant. Tested with Xcode 11.4 / iOS 13.4
struct AddressTextField: UIViewRepresentable {
var commit: () -> Void = {}
#EnvironmentObject var userData: UserDataModel
func makeUIView(context: UIViewRepresentableContext<AddressTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.text = self.userData.currentDisplayedAddress
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ textField: UITextField, context: UIViewRepresentableContext<AddressTextField>) {
if textField.text != self.userData.currentDisplayedAddress {
textField.text = self.userData.currentDisplayedAddress
}
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, UITextFieldDelegate {
var addressTextField: AddressTextField
init(_ addressTextField: AddressTextField) {
self.addressTextField = addressTextField
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool { //delegate method
textField.resignFirstResponder()
addressTextField.userData.currentDisplayedAddress = textField.text ?? String()
addressTextField.commit()
return true
}
}
}

How to declare GMSMapViewDelegate on SwiftUI

I'm trying to implement a solution with GoogleMapsApi with a map where user can touch the map and do things. For that, I understand delegate must be implemented, but I can't figure out how to achieve that with SwifUI. There's a lot of code samples on web, when in Swift, or even Objective C, but I couldn't find any on SwifUI.
Here's what I did (I'm trying to keep this code as simple as it could be):
struct GoogleMapsHomeView: UIViewRepresentable {
func makeUIView(context: Self.Context) -> GMSMapView {
let mapView = GMSMapView.map()
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
}
}
struct HomeView: View {
var body: some View {
GoogleMapsHomeView()
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
How to declare GMSMapViewDelegate and related listener for a user map moving detection?
The common pattern is to use coordinator as delegate
struct GoogleMapsHomeView: UIViewRepresentable {
func makeUIView(context: Self.Context) -> GMSMapView {
let mapView = GMSMapView.map()
mapView.delegate = context.coordinator
return mapView
}
func makeCoordinator() -> Coordinator {
Coordinator(owner: self)
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
}
class Coordinator: NSObject, GMSMapViewDelegate {
let owner: GoogleMapsHomeView // access to owner view members,
init(owner: GoogleMapsHomeView) {
self.owner = owner
}
// ... delegate methods here
}
}

How do I pass a string from UIViewController back to a SwiftUI View (iOS)

I need to use AVFoundation for an app I'm writing. Basically scanning a barcode, and sending that information back to another ViewController. This was pretty easy/straight forward with Swift and UIKit and I had this working.
Now, I launch the ViewController using a sheet (passing in the #State variable so I can dismiss the sheet later):
.sheet(isPresented: $isShowingCamera, content: {
ScanItem(isPresented: self.$isShowingCamera)
})
ScanItem is a UIViewRepresentable
Here are the functions in ScanItem:
func makeCoordinator() -> ScanItem.Coordinator {
return Coordinator(self)
}
public typealias UIViewControllerType = ScanBarcodeViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<ScanItem>) -> ScanBarcodeViewController {
return ScanBarcodeViewController()
}
func updateViewController(_ uiViewController: ScanBarcodeViewController, context: UIViewControllerRepresentableContext<ScanItem>) {
}
Inside I have the required methods and its displaying another UIViewController I created that uses AVFoundation to display the camera, and look for the barcode. Where I believe I need to progress is making the Coordinator handle the AVCaptureMetaData. I have the Coordinator like below:
class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
let parent: ScanItem
init(_ parent: ScanItem) {
self.parent = parent
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
let metaDataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject
if metaDataObj.stringValue != nil {
self.parent.$metadata = metadataObj.stringValue
self.parent.$isPresented = false
}
}
I think I'm on the right track here. This function gets called normally in the extension of the viewcontroller as AVCaptureMetadataOutputObjectsDelegate. I think I need to set the Coordinator as the delegate, call the function, set some bindable variable (#Bindable var metadata: String) and handle it in the SwiftUI view.
My current errors:
ScanBarcodeViewController (my viewcontoller to load the camera) cannot be constructed because it has no accessible initializers
Which goes along with
class ScanBarcodeViewController has not initializers
self.parent.$metaData = metaDataObj.stringValue -> cannot assign value of type String to type Binding -- fixed
self.parent.$isPresented = false -> cannot assign value of type Bool to type Binding -- fixed
Instead of passing self in Coordinator (which is struct, so copied), ie
Coordinator(self)
use binding to your model directly, so you can modify it, ie like
func makeCoordinator() -> Coordinator {
return Coordinator(data: $metadata)
}
and in Coordinator..
final class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
var data: Binding<String>
...
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
let metaDataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject
if metaDataObj.stringValue != nil {
...
data.wrappedValue = metadataObj.stringValue
...
}
}