In iOS 16, when can use ShareLink to present a share sheet. Is there a way to get notified when user swipe down or close this view? Need to do something after the share sheet is dismissed.
I was struggling with this too. As far as I can tell, ShareLink doesn't yet have a way to run code on dismiss. What worked very well for me was adapting UIActivityViewController for SwiftUI, and then presenting the share view in a sheet.
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [Any]
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
}
Usage:
Button(action: {
presentShareSheet = true
}) {
Text("Share Link")
}
.sheet(isPresented: $presentShareSheet, onDismiss: {
// Code to run on dismiss
}) {
ActivityViewController(activityItems: ["Check out this link!", URL(string: "https://www.google.com")!])
}
Related
I am trying to make UIActivityViewController stop showing localized contents such as translated apps and etc... to make it displays only in English. My app contains two localization English and Arabic.
I have added Localized resources can be mixed = NO to the info plist file but no success. Is there any way to stop localization for UIActivityViewController? Here is my codes:
struct ActivityViewController: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
print("activityItems=\(activityItems)")
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
controller.view.semanticContentAttribute = .forceLeftToRight
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
}
I'm trying to implement a PHPickerViewController using SwiftUI and The Composable Architecture. (Not that I think that's particularly relevant but it might explain why some of my code is like it is).
Sample project
I've been playing around with this to try and work it out. I created a little sample Project on GitHub which removes The Composable Architecture and keeps the UI super simple.
https://github.com/oliverfoggin/BrokenImagePickers/tree/main
It looks like iOS 15 is breaking on both the UIImagePickerViewController and the PHPickerViewController. (Which makes sense as they both use the same UI under the hood).
I guess the nest step is to determine if the same error occurs when using them in a UIKit app.
My code
My code is fairly straight forward. It's pretty much just a reimplementation of the same feature that uses UIImagePickerViewController but I wanted to try with the newer APIs.
My code looks like this...
public struct ImagePicker: UIViewControllerRepresentable {
// Vars and setup stuff...
#Environment(\.presentationMode) var presentationMode
let viewStore: ViewStore<ImagePickerState, ImagePickerAction>
public init(store: Store<ImagePickerState, ImagePickerAction>) {
self.viewStore = ViewStore(store)
}
// UIViewControllerRepresentable required functions
public func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> some UIViewController {
// Configuring the PHPickerViewController
var config = PHPickerConfiguration()
config.filter = PHPickerFilter.images
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// This is the coordinator that acts as the delegate
public class Coordinator: PHPickerViewControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let itemProvider = results.first?.itemProvider,
itemProvider.canLoadObject(ofClass: UIImage.self) else {
return
}
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
if let image = image as? UIImage {
DispatchQueue.main.async {
self?.parent.viewStore.send(.imagePicked(image: image))
}
}
}
}
}
}
All this works in the simple case
I can present the ImagePicker view and select a photo and it's all fine. I can cancel out of it ok. I can even scroll down the huge collection view of images that I have. I can even see the new image appear in my state object and display it within my app. (Note... this is still WIP and so the code is a bit clunky but that's only to get it working initially).
The problem case
The problem is that when I tap on the search bar in the PHPickerView (which is a search bar provided by Apple in the control, I didn't create it or code it). It seems to start to slide up the keyboard and then the view goes blank with a single message in the middle...
Unable to Load Photos
[Try Again]
I also get a strange looking error log. (I removed the time stamps to shorten the lines).
// These happen on immediately presenting the ImagePicker
AppName[587:30596] [Picker] Showing picker unavailable UI (reason: still loading) with error: (null)
AppName[587:30596] Writing analzed variants.
// These happen when tapping the search bar
AppName[587:30867] [lifecycle] [u A95D90FC-C77B-43CC-8FC6-C8E7C81DD22A:m (null)] [com.apple.mobileslideshow.photospicker(1.0)] Connection to plugin interrupted while in use.
AppName[587:31002] [lifecycle] [u A95D90FC-C77B-43CC-8FC6-C8E7C81DD22A:m (null)] [com.apple.mobileslideshow.photospicker(1.0)] Connection to plugin invalidated while in use.
AppName[587:30596] [Picker] Showing picker unavailable UI (reason: crashed) with error: (null)
AppName[587:30596] viewServiceDidTerminateWithError:: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted}
Tapping the "Try Again" button reloads the initial scroll screen and I can carry on using it. But tapping the search bar again just shows the same error.
I'm usually the first one to point out that the error is almost definitely not with the Apple APIs but I'm stumped on this one. I'm not sure what it is that I'm doing that is causing this to happen?
Is it the fact that it's in a SwiftUI view?
Recreated the project in UIKit
I remade the same project using UIKit... https://github.com/oliverfoggin/UIKit-Image-Pickers
And I couldn't replicate the crash at all.
Also... if you are taking any sort of screen recording of the device the crash will not happen. I tried taking a recording on the device itself and couldn't replicate it. I also tried doing a movie recording from my Mac using the iPhone screen and couldn't replicate the crash. But... the instant I stopped the recording on QuickTime the crash was replicable again.
This fixed it for me .ignoreSafeArea(.keyboard) like #Frustrated_Student mentions.
To elaborate on #Frustrated_Student this issue has to do with the UIViewControllerRepresentable treating the view like many SwiftUI views to automatically avoid the keyboard. If you are presenting the picker using a sheet as I am then you can simply add the .ignoreSafeArea(.keyboard) to the UIViewControllerRepresentable view in my case I called it ImagePicker here is a better example.
Where to add it the .ignoreSafeArea(.keyboard)
.sheet(isPresented: $imagePicker) {
ImagePicker(store: store)
.ignoresSafeArea(.keyboard)
}
This is #Fogmeister code:
public struct ImagePicker: UIViewControllerRepresentable {
// Vars and setup stuff...
#Environment(\.presentationMode) var presentationMode
let viewStore: ViewStore<ImagePickerState, ImagePickerAction>
public init(store: Store<ImagePickerState, ImagePickerAction>) {
self.viewStore = ViewStore(store)
}
// UIViewControllerRepresentable required functions
public func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> some UIViewController {
// Configuring the PHPickerViewController
var config = PHPickerConfiguration()
config.filter = PHPickerFilter.images
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// This is the coordinator that acts as the delegate
public class Coordinator: PHPickerViewControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let itemProvider = results.first?.itemProvider,
itemProvider.canLoadObject(ofClass: UIImage.self) else {
return
}
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
if let image = image as? UIImage {
DispatchQueue.main.async {
self?.parent.viewStore.send(.imagePicked(image: image))
}
}
}
}
}
}
Well.. this seems to be an iOS bug.
I have cerated a sample project here that shows the bug... https://github.com/oliverfoggin/BrokenImagePickers
And a replica project here written with UIKit that does not... https://github.com/oliverfoggin/UIKit-Image-Pickers
I tried to take a screen recording of this happening but it appears that if any screen recording is happening (whether on device or via QuickTime on the Mac) this suppresses the bug from happening.
I have filed a radar with Apple and sent them both projects to have a look at and LOTS of detail around what's happening. I'll keep this updated with any progress on that.
Hacky workaround
After a bit of further investigation I found that you can start with SwiftUI and then present a PHPickerViewController without this crash happening.
From SwiftUI if you present a UIViewControllerRepresentable... and then from there if you present the PHPickerViewController it will not crash.
So I came up with a (very tacky) workaround that avoids this crash.
I first create a UIViewController subclass that I use like a wrapper.
class WrappedPhotoPicker: UIViewController {
var picker: PHPickerViewController?
override func viewDidLoad() {
super.viewDidLoad()
if let picker = picker {
present(picker, animated: false)
}
}
}
Then in the SwiftUI View I create this wrapper and set the picker in it.
struct WrappedPickerView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var photoPickerResult: PHPickerResult?
let wrappedPicker = WrappedPhotoPicker()
func makeUIViewController(context: Context) -> WrappedPhotoPicker {
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
wrappedPicker.picker = picker
return wrappedPicker
}
func updateUIViewController(_ uiViewController: WrappedPhotoPicker, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: PHPickerViewControllerDelegate {
let parent: WrappedPickerView
init(_ parent: WrappedPickerView) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
parent.presentationMode.wrappedValue.dismiss()
parent.wrappedPicker.dismiss(animated: false)
parent.photoPickerResult = results.first
}
}
}
This is far from ideal as I'm presenting at the wrong time and stuff. But it works until Apple provide a permanent fix for this.
I started getting a weird UI bug after the PHPickerViewController crashed where the keyboard was not visible but my views were still being squashed. So I suspected a keyboard / avoidance issue. I disabled keyboard avoidance in a parent view and managed to stop it from crashing.
.ignoresSafeArea(.keyboard)
.... still a iOS bug in 15.0. I've modified Fogmeister's class Coordinator to return the image in addition to the PHPickerResult.
struct WrappedPickerView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var photoPickerResult: PHPickerResult?
#Binding var image: UIImage?
let wrappedPicker = WrappedPhotoPicker()
func makeUIViewController(context: Context) -> WrappedPhotoPicker {
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
wrappedPicker.picker = picker
return wrappedPicker
}
func updateUIViewController(_ uiViewController: WrappedPhotoPicker, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: PHPickerViewControllerDelegate {
let parent: WrappedPickerView
init(_ parent: WrappedPickerView) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
self.parent.presentationMode.wrappedValue.dismiss()
self.parent.wrappedPicker.dismiss(animated: false)
self.parent.photoPickerResult = results.first
print(results)
guard let result = results.first else {
return
}
self.parent.image = nil
DispatchQueue.global().async {
result.itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in
guard let imageLoaded = object as? UIImage else {
return
}
DispatchQueue.main.async {
self.parent.image = imageLoaded
}
}
}
}
}
}
I try to present a CNContactPickerViewController inside a SwiftUI application using the UIViewControllerRepresentable protocol. As I already read, there seems to be a known issue for this not working, but I got it working quite ok using the workaround described here.
However, whenever the CNContactPickerViewController gets presented or dismissed resp., I get the following error in my output log:
[PPT] Error creating the CFMessagePort needed to communicate with PPT.
I tried to find explanations on this, but there seems to be no answer anywhere on the internet. Does someone know where this error comes from and what PPT is? Could this error have something to do with the CNContactPickerViewController not working properly with SwiftUI?
I noticed the error for the first time in the iOS 14 beta together with the Xcode 12 beta, and it is still present in iOS 14.2 with Xcode 12.2.
I don't know if the error appears on iOS 13 as well.
I already issued a feedback report about this.
I wrote a workaround using a hosting UINavigationController and here is my code:
import SwiftUI
import ContactsUI
struct ContactPickerView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
func makeUIViewController(context: Context) -> UINavigationController {
let navController = UINavigationController()
let controller = CNContactPickerViewController()
controller.delegate = context.coordinator
navController.present(controller, animated: false, completion: nil)
return navController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
print("Updating the contacts controller!")
}
// MARK: ViewController Representable delegate methods
func makeCoordinator() -> ContactsCoordinator {
return ContactsCoordinator(self)
}
class ContactsCoordinator : NSObject, UINavigationControllerDelegate, CNContactPickerDelegate {
let parent: ContactPickerView
public init(_ parent: ContactPickerView) {
self.parent = parent
}
func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
print("Contact picked cancelled!")
parent.presentationMode.wrappedValue.dismiss()
}
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
print("Selected a contact")
parent.presentationMode.wrappedValue.dismiss()
}
}
}
And I use it like:
Button("Select a contact") {
openSelectContact.toggle()
}
.sheet(isPresented: $openSelectContact, onDismiss: nil) {
ContactPickerView()
}
I've been grinding on this issue for quite a few days now and it seems like SwiftUI's relative "newness" doesn't seem to help me with this.
My gut feeling is that I'm somehow using CNContactViewControllerDelegate wrong but both Apple's documentation as well as other questions on SO make it seem like it should work. Maybe, it is also caused by .sheet's handling, but I wasn't able to isolate the issue as well. Not wrapping NewContactView inside NavigationView also made no difference and removed the default navigation bar (as expected).
I'm showing the CNContactViewController with init(forNewContact:) to give the app's users an ability to add new contacts. Persisting and everything works fine, however, none of the delegate's functions seem to get called.
Neither dismissing the modal with the "swipe to dismiss" gesture introduced in iOS 13 calls the delegate function, nor using the navigation buttons provided by CNContactViewController.
import Foundation
import SwiftUI
import ContactsUI
struct NewContactView: UIViewControllerRepresentable {
class Coordinator: NSObject, CNContactViewControllerDelegate, UINavigationControllerDelegate {
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
if let c = contact {
self.parent.contact = c
}
viewController.dismiss(animated: true)
}
func contactViewController(_ viewController: CNContactViewController, shouldPerformDefaultActionFor property: CNContactProperty) -> Bool {
return true
}
var parent: NewContactView
init(_ parent: NewContactView) {
self.parent = parent
}
}
#Binding var contact: CNContact
init(contact: Binding<CNContact>) {
self._contact = contact
}
typealias UIViewControllerType = CNContactViewController
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<NewContactView>) -> NewContactView.UIViewControllerType {
let vc = CNContactViewController(forNewContact: CNContact())
vc.delegate = makeCoordinator()
return vc
}
func updateUIViewController(_ uiViewController: NewContactView.UIViewControllerType, context: UIViewControllerRepresentableContext<NewContactView>) {
}
}
The view's code for showing the controller looks like this:
.sheet(isPresented: self.$viewModel.showNewContact, onDismiss: { self.viewModel.fetchContacts() }) {
NavigationView() {
NewContactView(contact: self.$viewModel.newContact)
}
}
Thank you for any pointers! Rubberducking sadly didn't help...
SwiftUI creates coordinator by itself and provides it to representable in context, so just use
func makeUIViewController(context: UIViewControllerRepresentableContext<NewContactView>) -> NewContactView.UIViewControllerType {
let vc = CNContactViewController(forNewContact: CNContact())
vc.delegate = context.coordinator // << here !!
return vc
}
I have a project using SwiftUI that requires CloudKit sharing, but I'm unable to get the UICloudSharingController to play nice in a SwiftUI environment.
First Problem
A straight-forward wrap of UICloudSharingController using UIViewControllerRepresentable yields an endless spinner (see this). As has been done for other system controllers like UIActivityViewController, I wrapped the UICloudSharingController in a containing UIViewController like this:
struct CloudSharingController: UIViewControllerRepresentable {
#EnvironmentObject var store: CloudStore
#Binding var isShowing: Bool
func makeUIViewController(context: Context) -> CloudControllerHost {
let host = CloudControllerHost()
host.rootRecord = store.noteRecord
host.container = store.container
return host
}
func updateUIViewController(_ host: CloudControllerHost, context: Context) {
if isShowing, host.isPresented == false {
host.share()
}
}
}
final class CloudControllerHost: UIViewController {
var rootRecord: CKRecord? = nil
var container: CKContainer = .default()
var isPresented = false
func share() {
let sharingController = shareController
isPresented = true
present(sharingController, animated: true, completion: nil)
}
lazy var shareController: UICloudSharingController = {
let controller = UICloudSharingController { [weak self] controller, completion in
guard let self = self else { return completion(nil, nil, CloudError.controllerInvalidated) }
guard let record = self.rootRecord else { return completion(nil, nil, CloudError.missingNoteRecord) }
let share = CKShare(rootRecord: record)
let operation = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: [])
operation.modifyRecordsCompletionBlock = { saved, _, error in
if let error = error {
return completion(nil, nil, error)
}
completion(share, self.container, nil)
}
self.container.privateCloudDatabase.add(operation)
}
controller.delegate = self
controller.popoverPresentationController?.sourceView = self.view
return controller
}()
}
This allows the controller to come up normally, but...
Second Problem
Tap the close button or swipe to dismiss and the controller will disappear, but there's no notification that it's been dismissed. The SwiftUI view's #State property that initiated presenting the controller is still true. There's no obvious method to detect dismissal of the modal. After some experimenting, I discovered the presenting controller is the original UIHostingController created in the SceneDelegate. With some hackery, you can inject an object that is referenced in a UIHostingController subclass into the CloudSharingController. This will let you detect the dismissal and set the #State property to false. However, all nav bar buttons no longer function after dismissing so you could only ever tap this thing once. The rest of the scene is completely functional, but buttons in the nav bar don't respond.
Third Problem
Even if you could get the UICloudSharingController to present and dismiss normally, tapping on any of the sharing methods (Messages, Mail, etc) makes the controller disappear with no animation and the controller for the sharing URL doesn't come up. No crash or console messages--it just disappears.
Demo
I made a quick and dirty project on GitHub to demonstrate the issue: CloudKitSharing. It just creates a single String and a CKRecord to represent it using CloudKit. The interface displays the String (a UUID) with a single nav bar button to share it:
The Plea
Is there any way to use UICloudSharingController in SwiftUI? Don't have the time to rebuild the project in UIKit or a custom sharing controller (I know--the price of being on the bleeding edge 💩)
I got this working -- initially, I wrapped the UICloudSharingController in a UIViewControllerRepresentable, much like the link you provided (I referenced that while building it), and simply adding it to a SwiftUI .sheet() view. This worked on the iPhone, but it failed on the iPad, because it requires you to set the popoverPresentationController?.sourceView, and I didn't have one, given that I triggered the sheet with a SwiftUI Button.
Going back to the drawing board, I rebuilt the button itself as a UIViewRepresentable, and was able to present the view using the rootViewController trick that SeungUn Ham suggested here. All works, on both iPhone and iPad - at least in the simulator.
My button:
struct UIKitCloudKitSharingButton: UIViewRepresentable {
typealias UIViewType = UIButton
#ObservedObject
var toShare: ObjectToShare
#State
var share: CKShare?
func makeUIView(context: UIViewRepresentableContext<UIKitCloudKitSharingButton>) -> UIButton {
let button = UIButton()
button.setImage(UIImage(systemName: "person.crop.circle.badge.plus"), for: .normal)
button.addTarget(context.coordinator, action: #selector(context.coordinator.pressed(_:)), for: .touchUpInside)
context.coordinator.button = button
return button
}
func updateUIView(_ uiView: UIButton, context: UIViewRepresentableContext<UIKitCloudKitSharingButton>) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UICloudSharingControllerDelegate {
var button: UIButton?
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
//Handle some errors here.
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return parent.toShare.name
}
var parent: UIKitCloudKitSharingButton
init(_ parent: UIKitCloudKitSharingButton) {
self.parent = parent
}
#objc func pressed(_ sender: UIButton) {
//Pre-Create the CKShare record here, and assign to parent.share...
let sharingController = UICloudSharingController(share: share, container: myContainer)
sharingController.delegate = self
sharingController.availablePermissions = [.allowReadWrite]
if let button = self.button {
sharingController.popoverPresentationController?.sourceView = button
}
UIApplication.shared.windows.first?.rootViewController?.present(sharingController, animated: true)
}
}
}
Maybe just use rootViewController.
let window = UIApplication.shared.windows.filter { type(of: $0) == UIWindow.self }.first
window?.rootViewController?.present(sharingController, animated: true)