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)
}
}
Related
Iβm using UIViewControllerRepresentable for Document picker presentation in swiftUI. The issue is that I'm not able to select the video audio pdf it's all showing in a frozen manner. I need to fix this issues for the iOS 14 and above version. I'm able to select the file by tap-hold and then release its only works with the simulator in real devices itβs not possible for both the simulator and devices except for the file structure, the rest of the documents are in greyed out.
enter image description here
struct DocumentPicker: UIViewControllerRepresentable {
#ObservedObject var chatViewModel: RedesignChatViewModel
func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPicker>) -> UIDocumentPickerViewController {
let viewController = UIDocumentPickerViewController(forOpeningContentTypes: [.pdf, .mp3, .audio, .video, .movie, .item])
viewController.shouldShowFileExtensions = true
viewController.allowsMultipleSelection = false
viewController.delegate = context.coordinator
return viewController
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {
}
}
extension DocumentPicker {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIDocumentPickerDelegate {
var parent: DocumentPicker
var path: String?
init(_ documentPicker: DocumentPicker) {
self.parent = documentPicker
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else { return }
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
controller.dismiss(animated: true)
}
There are a few mistakes, first you'll need to create a UIViewController and use that to present the UIDocumentPickerViewController. Also you need to change Coordinator(self) to Coordinator() and in makeUIViewController return context.coordinator.myViewController that should be a lazy property. The reason for this is that self you are passing in is immediately out of date because it is a value type. You also need to remove the #ObservedObject and add lets or #Binding vars for your properties. When the repreresentable is init with new values for those properties, updateUIViewController will be called and you can then update the coordinator and view controller with the new values.
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 currently making use UITabBarController in SwiftUI. Here is the implementation:
struct MyTabView: View {
private var viewControllers: [UIHostingController<AnyView>]
public init( _ views: [AnyView]) {
self.viewControllers = views.map { UIHostingController(rootView:$0) }
}
public var body: some View {
return TabBarController(controllers: viewControllers)
.edgesIgnoringSafeArea(.all)
}
}
struct TabBarController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UITabBarController {
let tabBarController = UITabBarController()
tabBarController.viewControllers = controllers
return tabBarController
}
func updateUIViewController(_ tabBarController: UITabBarController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITabBarControllerDelegate {
var parent: TabBarController
init(_ tabBarController: TabBarController) {
self.parent = tabBarController
}
}
}
Inside of my SwiftUI I have the following:
struct ContentView: View {
var body: some View {
MyTabView([
AnyView(Text("Moo Moo")),
AnyView(MyPage())
])
}
}
struct MyPage:View {
var body:some View {
NavigationView {
VStack {
ForEach((1...10).reversed(), id: \.self) { value -> AnyView in
print("For Each Value Called")
return AnyView(MyView(text: String(value)))
}
}
}
}
}
struct MyView:View {
let text:String
var body:some View {
Text(text).onAppear {
print("On Appear Called - Making Service Call for \(text)")
}
}
}
I have the following questions:
When running this code the On Appear Called - Making Service Call for \(text), is called twice. What would cause this? My expectation is that it is only run once. Should this be occurring?
Is this a SwiftUI bug lurking around or is this expected behaviour?
Yes, your expectation would be correct. However, it looks like a bug.
The problem appear when having content inside NavigationView. If you use .onAppear() on the NavigationView, you will see it called only once. If you use onAppear() on the VStack, it's already twice.
This has reported in this thread aswell
From my view, this behavior is wrong. Maybe report to Apple or ask why
maybe I found a solution:
add on every very first NavigationLink the modifier .isDetailLink(false)
for me it stops the double onAppear calls
Just getting started with SwiftUI.
I have a GoogleMapsView in a ContentView
using the CLLocationManager I capture events in the AppDelegate or SceneDelegate class by means of extending them with CLLocationManagerDelegate.
How can I invoke a method in the GoogleMapsView from the AppDelegate or SceneDelegate?
In this instance I want to call the .animate method when the location change event is sent to the AppDelegate instance via the CLLocationManagerDelegate, but the question is really more generic.
I made and implementation of CLLocationManager and MKMapView and it is almost the same as maps, hope it will help you:
Short answer: declaring a #Binding var foo: Any you will be able to make changes inside GoogleMapView every time that foo changes, in this case foo is your location, so you can call animate every time foo is updated.
Long answer:
First I created a Mapview that conforms UIViewRepresentable protocol, just as you did, but adding a #Binding variable, this is my "trigger".
MapView:
struct MapView: UIViewRepresentable {
#Binding var location: CLLocation // Create a #Binding variable that keeps the location where I want to place the view, every time it changes updateUIView will be called
private let zoomMeters = 400
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MKMapView {
let mapView = MKMapView(frame: UIScreen.main.bounds)
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
//When location changes, updateUIView is called, so here I move the map:
let region = MKCoordinateRegion(center: location.coordinate,
latitudinalMeters: CLLocationDistance(exactly: zoomMeters)!,
longitudinalMeters: CLLocationDistance(exactly: zoomMeters)!)
mapView.setRegion(mapView.regionThatFits(region), animated: true)
}
}
Then I placed my MapView in my ContentView, passing a location argument, which I will explain next:
ContentView:
struct ContentView: View {
#ObservedObject var viewModel: ContentViewModel
var body: some View {
VStack {
MapView(location: self.$viewModel.location)
}
}
}
In my ViewModel, I handle location changes using a delegate, here is the code with more details in comments:
class ContentViewModel: ObservableObject {
//location is a Published value, so the view is updated every time location changes
#Published var location: CLLocation = CLLocation.init()
//LocationWorker will take care of CLLocationManager...
let locationWorker: LocationWorker = LocationWorker()
init() {
locationWorker.delegate = self
}
}
extension ContentViewModel: LocationWorkerDelegate {
func locationChanged(lastLocation: CLLocation?) {
//Location changed, I change the value of self.location, it is a #Published value so it will refresh the #Binding variable inside MapView and call MapView.updateUIView
self.location = CLLocation.init(latitude: lastLocation!.coordinate.latitude, longitude: lastLocation!.coordinate.latitude)
}
}
And finally here is LocationWorker which take cares of CLLocationManager():
class LocationWorker: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
var delegate: LocationWorkerDelegate?
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var locationStatus: CLAuthorizationStatus? {
willSet {
objectWillChange.send()
}
}
#Published var lastLocation: CLLocation? {
willSet {
objectWillChange.send()
}
}
override init() {
super.init()
self.locationManager.delegate = self
//...
}
}
protocol LocationWorkerDelegate {
func locationChanged(lastLocation: CLLocation?)
}
extension LocationWorker: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
self.lastLocation = location
//When location changes: I use my delegate ->
if delegate != nil {
delegate!.locationChanged(lastLocation: lastLocation)
}
}
}
Instead of calling a View method directly from outside, you should revise your logic a bit and just change some kind of a state somewhere and let the View update itself. Take a look at this algorithm:
The classic (and worst) way:
Location changed
Delegate method called in the app delegate (Better refactor to else where)
App delegate calls a method directly on the view (You should pass a reference to that view all the way up to the app delegate)
Although the above algorithm is what you are looking for originally, It isn't the best way and I don't recommend it at all! But it will work π€·π»ββοΈ
The SwiftUI way:
Location changed
Delegate method called in the responsible object (maybe a singleton location location manager instance π€·π»ββοΈ)
Location manager updates a State somewhere. (maybe an ObservedObject variable inside itself or an EnvironmentObject or etc.)
All views that subscribed for changes of that property will notify about the changes
All notified views will update themselves.
This is how it should be done. But there are more than just one way to implement this and you should consider your preferences to pick the best for you.
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
...
}
}