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.
Related
I am having an issue with my Coordinator. I am interacting with a MKMapView via SwiftUI. I am passing in a Binding to the UIViewRepresentable and need to access that same Binding in the Coordinator. Inside the Coordinator I determine what strokeColor to use for my polyline. When I try to access the routes Binding from my Coordinator it is always empty. When I set a breakpoint inside the MapView on the updateUIView function the binding is indeed populated.
Heres the code:
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
var region: MKCoordinateRegion
#Binding var routes: [RouteData]
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = true
mapView.setRegion(region, animated: true)
addOverlays(mapView)
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
addOverlays(view)
removeOverlays(view)
}
private func addOverlays(_ view: MKMapView) {
for route in routes {
for point in route.points {
let waypoints = point.waypoints
let polyline = MKPolyline(coordinates: waypoints, count: waypoints.count)
polyline.title = route.routeID
view.addOverlay(polyline)
}
}
}
private func removeOverlays(_ view: MKMapView) {
for overlay in view.overlays {
if let routeID = overlay.title!, routes.first(where: { $0.routeID == routeID }) == nil {
view.removeOverlay(overlay)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
class Coordinator: NSObject, MKMapViewDelegate {
let parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let routePolyline = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(polyline: routePolyline)
// Always prints route is empty even though I set a break point inside the parents' updateUIView func and the route is populated.
print("parents routes: \(self.parent.routes)")
if let title = routePolyline.title, let route = self.parent.routes.first(where: { $0.routeID == title }) {
renderer.strokeColor = UIColor(convertRGBStringToColor(color: route.route.rtclr))
} else {
renderer.strokeColor = UIColor.blue
}
renderer.lineWidth = 5
return renderer
}
return MKOverlayRenderer()
}
}
A few mistakes
#Binding var routes: [RouteData] should be let routes: [RouteData] because you don’t change it so don’t need the write access.
Coordinator(self) Should be Coordinator(), self is an old value the Coordinator should not hang on to.
Subclass MKPolyline to add your colour properties eg https://stackoverflow.com/a/44294417/259521
makeUIView Should return context.coordinator.mapView
addOverlays should only add ones that are not already added. You need to essentially implement a diff in updateUIView.
Update is called after make so no need to add overlays in make.
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 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 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
...
}
}
So I have a ParentView, which has a NavigationLink, leading to a UIViewControllerRepresentable-conforming PageViewController.
Now that ParentView also has some subscription on some publisher. Whenever that one is fired, not only will the ParentView redraw all its content (which it should), it will also re-initialize the (already presenting) PageViewController.
That leads to stuttering/glitching, because the PageViewController is already presenting and using the controllers that are continually being resetted.
Below is the ParentView and PageViewController (without the Coordinator stuff), both is pretty vanilla. The commented guard line is a hack I tried to prevent it from updating if displayed already. It helps but it's still stuttering on every swipe.
So the question is: How can we prevent the updating of a presented ViewController-wrapped-View when its presenting View is redrawn?
struct ParentView: View {
#Binding var something: Bool
var body: some View {
NavigationLink(destination: PageViewController(controllers: controllers)) {
Text("Push me")
}
}
}
final class PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
private var currentPage = 0
init(controllers: [UIViewController]) {
self.controllers = controllers
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
// I tried this: guard pageViewController.viewControllers!.isEmpty else { return }
pageViewController.setViewControllers(
[controllers[currentPage]], direction: .forward, animated: true)
}
}
If your controllers don't change after being displayed once, you can simply call:
pageViewController.setViewControllers([controllers[currentPage]], direction: .forward, animated: true)
from the makeUIViewController(context:) function instead of the updateUIViewController(:) function.