0

I am working on a SwiftUI project that is using a UIImagePickerController through UIViewControllerRepresentable. In the UIViewControllerRepresentable file I add the following line so that I can dismiss the ImagePicker in imagePickerController didFinishPickingMediaWithOptions method.

@Environment(\.presentationMode) var presentationMode

In my view file I would also like to be able to dismiss the view so I add the line.

@Environment(\.presentationMode) var presentaionMode: Binding<PresentationMode>

However, when this line is added to the view, it causes the ActionSheet to be created four time. Once an item is selected from the ActionSheet it is called twice more. The print statement ShowSheetButtons is how I've identified it being called multiple times. If this @Environment line is removed, all performs as expected. Is there a reason this is happening and how can I fix it. I'd like to be able to use presentationMode in both the View and the UIViewControllerRpresentable.

View

struct CreateListingView: View {
    // MARK: ++++++++++++++++++++++++++++++++++++++ Properties ++++++++++++++++++++++++++++++++++++++
    
    // Dismiss the View
    @Environment(\.presentationMode) var presentaionMode: Binding<PresentationMode>
    
    // Media Picker
    @State var showMediaPickerSheet = false
    @State var showLibrary = false
    @State var showMediaErrorAlert = false
    @frozen enum MediaTypeSource {
        case library
    }
    
    // MARK: ++++++++++++++++++++++++++++++++++++++ View ++++++++++++++++++++++++++++++++++++++
    var body: some View {
        
        return
        Button(action: {
            self.presentMediaPicker()
        }, label: {
            Text("Button")
        })
        
            .actionSheet(isPresented: $showMediaPickerSheet, content: {
                ActionSheet(
                    title: Text("Add Store Picture"),
                    buttons: sheetButtons()
                )
            }) // Action Sheet
            .sheet(isPresented: $showLibrary, content: {
                MediaPickerPhoto(sourceType: .photoLibrary, showError: $showMediaErrorAlert) { (image, error) in
                    if error != nil {
                        print(error!)
                    } else {
                        guard let image = image else {
                            return
                        }
                    }
                }
            })
    } // View
    
    
    // MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++
    
    // Media Picker Methods
    func presentMediaPicker() {
        print("PresentMediaPicker1Listing")
        self.showMediaPickerSheet = true
    }
    
    func sheetButtons() -> [Alert.Button] {
        print("ShowSheetButtonsListing")
        
        return UIImagePickerController.isSourceTypeAvailable(.camera) ? [
            .default(Text("Choose Photo")) {
                presentMediaPicker1(.library)
            },
            .cancel {
                showMediaPickerSheet = false
            }
        ] : [
            .default(Text("Choose Photo")) {
                presentMediaPicker1(.library)
            },
            .cancel {
                showMediaPickerSheet = false
            }
        ]
    }
    
    private func presentMediaPicker1(_ type: MediaTypeSource) {
        print("PresentMediaPicker2Listing")
        
        showMediaPickerSheet = false
        switch type {
        case .library:
            showLibrary = true
        }
    }
}

UIViewControllerRepresentable

struct MediaPickerPhoto: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIImagePickerController
    /// Presentation wrapper
    @Environment(\.presentationMode) var presentationMode

    /// Source type to present for
    let sourceType: UIImagePickerController.SourceType

    /// Binding for showing error alerts
    @Binding var showError: Bool

    /// Callback for media selection
    let completion: (UIImage?, String?) -> Void

    // MARK: - Representable

    func makeUIViewController(context: UIViewControllerRepresentableContext<MediaPickerPhoto>) -> UIImagePickerController {
        print("MakeUIViewController")
        let picker = UIImagePickerController()

        if sourceType == .camera {
            picker.videoQuality = .typeMedium
        }

        picker.sourceType = sourceType
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<MediaPickerPhoto>) {
        // no-op
    }

    // MARK: - Coordinator

    func makeCoordinator() -> MediaCoordinatorPhoto {
        return Coordinator(self)
    }
}

/// Coordinator for  media picker
class MediaCoordinatorPhoto: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    
    // AuthSession
    @EnvironmentObject var authSession: AuthSession
    
    let db = Firestore.firestore()
    
    /// Parent picker
    let parent: MediaPickerPhoto

    // MARK: - Init

    init(_ parent: MediaPickerPhoto) {
        print("MediaCoordinatorPhoto")

        self.parent = parent
    }

    // MARK: - Delegate

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        guard let type = (info[.mediaType] as? String)?.lowercased() else {
            return
        }

        if type.contains("image"), let uiImage = info[.originalImage] as? UIImage {
            // We attempt to resize the image to max os 1280x1280 for perf
            // If it fails, we use the original selection image
            let image = uiImage.resized(maxSize: CGSize(width: 1280, height: 1280)) ?? uiImage

            self.parent.completion(image, nil)
        } else {
            print("Invalid media type selected")
            let error = "There was an error updating your user profile picture. Please try again later."
            parent.completion(nil, error)
            //parent.showError = true
        }

        parent.presentationMode.wrappedValue.dismiss()
    }
}

Edited

2021-11-03 17:20:13.799510-0700 Global Store Exchange[6900:1991500] [lifecycle] [u

478AD6F6-A9E2-4FE9-96D0-D310B754DD59:m (null)] [com.apple.mobileslideshow.photo-picker(1.0)] Connection to plugin interrupted while in use. 2021-11-03 17:20:13.800173-0700 Global Store Exchange[6900:1990765] viewServiceDidTerminateWithError:: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted} 2021-11-03 17:20:13.800383-0700 Global Store Exchange[6900:1990765] [UI] -[PUPhotoPickerHostViewController viewServiceDidTerminateWithError:] Error Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted} 2021-11-03 17:20:13.800987-0700 Global Store Exchange[6900:1990765] viewServiceDidTerminateWithError:: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted} 2021-11-03 17:20:13.801016-0700 Global Store Exchange[6900:1990765] [UI] -[PUPhotoPickerHostViewController viewServiceDidTerminateWithError:] Error Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted} 2021-11-03 17:20:13.801089-0700 Global Store Exchange[6900:1990765] UIImagePickerController UIViewController create error: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.mobileslideshow.photo-picker.viewservice" UserInfo={NSDebugDescription=connection to service named com.apple.mobileslideshow.photo-picker.viewservice} 2021-11-03 17:20:13.801266-0700 Global Store Exchange[6900:1990765] viewServiceDidTerminateWithError:: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted} 2021-11-03 17:20:13.801313-0700 Global Store Exchange[6900:1990765] [UI] -[PUPhotoPickerHostViewController viewServiceDidTerminateWithError:] Error Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted} 2021-11-03 17:20:13.801421-0700 Global Store Exchange[6900:1990765] viewServiceDidTerminateWithError:: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted} 2021-11-03 17:20:13.801459-0700 Global Store Exchange[6900:1990765] [UI] -[PUPhotoPickerHostViewController viewServiceDidTerminateWithError:] Error Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted}

jonthornham
  • 2,881
  • 4
  • 19
  • 35
  • I tried your code with a simple parent view that displays `CreateListingView` and couldn't reproduce your issue. Can you make sure you have a [mre] included here? – jnpdx Nov 03 '21 at 19:08
  • Yes, I just built a new app and added CreateListingView to ContentView and added a MediaPickerPhoto file. While ShowSheetButtons only fires twice in this case it still causes the problem. The issue happens on both device and simulator. You can find a repo of this simple project here. https://github.com/jonthornham/EnvironmentTest2 – jonthornham Nov 03 '21 at 22:59
  • Okay, I'm running the code. I do see `ShowSheetButtons` printed four times, which means that the `ActionSheet` is rendered multiple times. But, I'm not necessarily seeing this as in inherent issue -- SwiftUI views are often recreated/re-rendered. In this case, it must be because the `presentationMode` gets recalculated when the sheet shows up. Is this causing an unwarranted side-effect? – jnpdx Nov 03 '21 at 23:58
  • Yes. In the project I’m working on I allow a user to upload multiple photos as part of a marketplace listing. After multiple calls, the image picker loads blank and crashes. This does not happen for the camera. When the Environment is removed the issue goes away. – jonthornham Nov 04 '21 at 00:07
  • Ah -- interesting. In the GitHub repo, the `presentaionMode: Binding` in `ContentView` doesn't seem to be used at all -- but in your actual app, it's key that it's there, right? Is there a way to represent that in the example so we can try to find a way around it? – jnpdx Nov 04 '21 at 00:15
  • I just added the crash report. I haven’t even used presentationMode yet. Simply having it there causes the issue. I will use it to pop a navigation view after a method call. – jonthornham Nov 04 '21 at 00:25

1 Answers1

0

I figured out a work around to this issue. Instead of using @EnvironmentObject I am using a Bool and the isActive property in NavigationLink.

In the view that presents the CreateListingView I add a Toolbar to the NavigationView. In the ToolbarItem I toggle an @State variable that is connected to the isActive property.

struct MarketplaceView: View {
    @State private var presentCreateListingView = false

    @EnvironmentObject var marketplaceViewModel: MarketplaceViewModel

    var body: some View {
        NavigationView {
                List {
                    ForEach(self.marketplaceViewModel.filteredListingRowViewModels, id: \.id) { listingRowViewModel in
                            NavigationLink(destination: ListingDetailView()) {
                                ListingRowView(listingRowViewModel: listingRowViewModel)
                            }
                    } // ForEach
                } // List
            .navigationTitle("Marketplace")
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    NavigationLink(destination: CreateListingView(presentCreateListingView: self.$presentCreateListingView), isActive: self.$presentCreateListingView, label: {
                        Button(action: {
                            self.presentCreateListingView.toggle()
                        }, label: {
                            Image(systemName: "plus")
                        })
                    }) // NavigationLink
                } // ToolbarItem
            } // Toolbar
        } // NavigationView
        //.navigationViewStyle(StackNavigationViewStyle())
    } // View
}

Then in the CreateListingView I have an @Binding to the Bool. Then I use a Button to toggle the Bool.

struct CreateListingView: View {
    @Binding var presentCreateListingView: Bool
    
    var body: some View {
        Button(action: {
            self.presentCreateListingView.toggle()
        }, label: {
            Text("Back")
        })
    } // View
}

I hope this helps someone out.

jonthornham
  • 2,881
  • 4
  • 19
  • 35