SwiftUI Send SMS with Coordinator and UIViewControllerRepresentable - swiftui

How can I write the following code with UIViewControllerRepresentable?
I want to use UIViewControllerRepresentable. I want the code I write to be regular. Do you think it makes sense to write this code with UIViewControllerRepresentable?
import MessageUI
class ViewController: UIViewController, MFMessageComposeViewControllerDelegate {
#IBOutlet weak var phoneNumber: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func sendText(sender: UIButton) {
if (MFMessageComposeViewController.canSendText()) {
let controller = MFMessageComposeViewController()
controller.body = "Message Body"
controller.recipients = [phoneNumber.text]
controller.messageComposeDelegate = self
self.presentViewController(controller, animated: true, completion: nil)
}
}
func messageComposeViewController(controller: MFMessageComposeViewController!, didFinishWithResult result: MessageComposeResult) {
//... handle sms screen actions
self.dismissViewControllerAnimated(true, completion: nil)
}
override func viewWillDisappear(animated: Bool) {
self.navigationController?.navigationBarHidden = false
}
}
SMSViewController
struct SMSViewController: UIViewControllerRepresentable {
class Coordinator: NSObject, MFMessageComposeViewControllerDelegate {
func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
}
var parent: MFMessageComposeViewController
init(_ parent: MFMessageComposeViewController) {
self.parent = parent
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MFMessageComposeViewController>) -> MFMessageComposeViewController {
let messageViewController = MFMessageComposeViewController()
messageViewController.delegate = context.coordinator
return messageViewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}

Related

How to make custom MKMapView delegate actions for SwiftUI ViewRepresentable?

I've wrapped a MKMapView in a ViewRepresentable
public struct MapView: UIViewRepresentable {
I want to create an action callback that works like this:
MapView().onAnnotationTapped { annotation in
}
In MapView i have defined
#inlinable public func onAnnotationTapped(site: (AnnotationView) -> ()) -> some View {
return self
}
But how to provide the AnnotationView from the coordinator?
public class MapViewCoordinator: NSObject, MKMapViewDelegate {
var mapView: MapView
init(_ control: MapView) {
self.mapView = control
}
public func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
/// How to send from here to the MapView function?
}
}
Here is a solution - introduce callback property for view and inject it in your inalinable modifier. Prepared with Xcode 12.1 / iOS 14.1
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
let view = MKMapView()
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
func makeCoordinator() -> MapViewCoordinator {
MapViewCoordinator(self)
}
var tapCallback: ((MKAnnotationView) -> ())? // << this one !!
#inlinable public func onAnnotationTapped(site: #escaping (MKAnnotationView) -> ()) -> some View {
var newMapView = self
newMapView.tapCallback = site // << here !!
return newMapView
}
public class MapViewCoordinator: NSObject, MKMapViewDelegate {
var mapView: MapView
init(_ control: MapView) {
self.mapView = control
}
public func mapView(_ mkMap: MKMapView, didSelect view: MKAnnotationView) {
self.mapView.tapCallback?(view) // << call !!
}
}
}

how to use #published and #observed with FSCalendar?

How to get for example selectedDate from FSCalendar?
struct CalendarController: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<CalendarController>)
-> calendars {
let calendarViewController = calendars()
return calendarViewController
}
func updateUIViewController(_ uiViewController: calendars, context:
UIViewControllerRepresentableContext<CalendarController>) {
}
func updateUIViewController(_ uiViewController: CalendarController, context:
UIViewControllerRepresentableContext<CalendarController>) {
}
class Coordinator: NSObject {
var parent: CalendarController
init(_ calendarViewController: CalendarController) {
self.parent = calendarViewController
}
}
}
class calendars: UIViewController, FSCalendarDelegate, ObservableObject {
var calendar = FSCalendar()
#Published var selectedData : Date?
override func viewDidLoad() {
super.viewDidLoad()
calendar.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
calendar.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height:
view.frame.size.width)
view.addSubview(calendar)
}
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition:
FSCalendarMonthPosition) {
print("didSelect date: \(date)")
selectedData = date
}
}
struct CalendarView: View {
var body: some View{
CalendarController().padding().frame(alignment: .top)
}
}
struct Calendar_Previews: PreviewProvider {
static var previews: some View {
CalendarView()
}
}
Later in another swift view I try to get the date, but it is never changed.
I use the
#ObservedObject var calendarData = calendars()
//...
CalendarView().scaledToFit()
Text("\(self.calendarData.selectedData ?? Date())")
the following works.
As I am new to iOS, swift, I could not figure out the trick with delegate.
Coordinator class shall implement it to make it work.
'''
import SwiftUI
import UIKit
import FSCalendar
import Combine
struct CalendarController: UIViewControllerRepresentable {
#Binding var selectedDate : Date?
func makeCoordinator() -> CalendarController.Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> calendars {
let calendar = calendars()
calendar.calendar.delegate = context.coordinator
return calendar
}
func updateUIViewController(_ uiViewController: calendars, context: Context) {
}
}
class calendars: UIViewController {
var calendar = FSCalendar()
var selectedDate : Date?
override func viewDidLoad() {
super.viewDidLoad()
calendar.delegate = self // delegate
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
calendar.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height:
view.frame.size.width)
view.addSubview(calendar)
}
}
extension CalendarController {
class Coordinator: NSObject, FSCalendarDelegate {
var parent: CalendarController
init(_ parent: CalendarController) {
self.parent = parent
}
func calendar(_ calendar: FSCalendar, didSelect date: Date, at
monthPosition: FSCalendarMonthPosition) {
print(date)
parent.selectedDate = date
}
}
}
'''

SKStoreProductViewController must be used in a modal view controller SWIFTUI

I am building a info page for my SwiftUI app. One item should open App Store, another mail. I have written UIViewControllerRepresentable for each.
MailView works fine totally. StoreView displays fine, but when pressed on Cancel button, throws exception
"*** Terminating app due to uncaught exception 'SKUnsupportedPresentationException', reason: 'SKStoreProductViewController must be used in a modal view controller'".
MailView goes fine into didFinish delegate method but StoreView does not go into didFinish delegate method, it crashes before going into this didFinish method. What am I doing wrong please?
import SwiftUI
import StoreKit
import MessageUI
struct InfoMoreAppsView: View {
#State var showAppAtStore = false
#State var reportBug = false
#State var result: Result<MFMailComposeResult, Error>? = nil
let otherAppName = "TheoryTest"
var body: some View {
VStack(alignment: .leading){
HStack{
Image(Helper.getOtherAppImageName(otherAppName: otherAppName))
Button(action: { self.showAppAtStore = true }) {
Text(otherAppName)
}
.sheet(isPresented: $showAppAtStore){
StoreView(appID: Helper.getOtherAppID(otherAppName: otherAppName))
}
}
Button(action: { self.reportBug = true }) {
Text("Report a bug")
}
.sheet(isPresented: $reportBug){
MailView(result: self.$result)
}
}
.padding()
.font(.title2)
}
}
struct StoreView: UIViewControllerRepresentable {
let appID: String
#Environment(\.presentationMode) var presentation
class Coordinator: NSObject, SKStoreProductViewControllerDelegate {
#Binding var presentation: PresentationMode
init(presentation: Binding<PresentationMode> ) {
_presentation = presentation
}
private func productViewControllerDidFinish(viewController: SKStoreProductViewController) {
$presentation.wrappedValue.dismiss()
viewController.dismiss(animated: true, completion: nil)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<StoreView>) -> SKStoreProductViewController {
let skStoreProductViewController = SKStoreProductViewController()
skStoreProductViewController.delegate = context.coordinator
let parameters = [ SKStoreProductParameterITunesItemIdentifier : appID]
skStoreProductViewController.loadProduct(withParameters: parameters)
return skStoreProductViewController
}
func updateUIViewController(_ uiViewController: SKStoreProductViewController, context: UIViewControllerRepresentableContext<StoreView>) {
}
}
struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let mailComposeViewController = MFMailComposeViewController()
mailComposeViewController.mailComposeDelegate = context.coordinator
mailComposeViewController.setToRecipients([Constants.SUPPORT_EMAIL])
mailComposeViewController.setMessageBody(systemInfo(), isHTML: true)
return mailComposeViewController
}
func systemInfo() -> String {
let device = UIDevice.current
let systemVersion = device.systemVersion
let model = UIDevice.hardwareModel
let mailBody = "Model: " + model + ". OS: " + systemVersion
return mailBody
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
This isn't very "Swifty" or pretty but I got this to work without crashing by not wrapping the SKStoreProductViewController in a representable.
struct MovieView: View {
var vc:SKStoreProductViewController = SKStoreProductViewController()
var body: some View {
HStack(){
Button(action: {
let params = [
SKStoreProductParameterITunesItemIdentifier:"1179624268",
SKStoreProductParameterAffiliateToken:"11l4Cu",
SKStoreProductParameterCampaignToken:"hype_movie"
] as [String : Any]
// vc!.delegate = self
vc.loadProduct(withParameters: params, completionBlock: { (success,error) -> Void in
UIApplication.shared.windows.first?.rootViewController?.present(vc, animated: true, completion: nil)
})
}) {
HStack {
Image(systemName: "play.fill")
.font(.headline)
}
.padding(EdgeInsets(top: 6, leading:36, bottom: 6, trailing: 36))
.foregroundColor(.white)
.background(Color(red: 29/255, green: 231/255, blue: 130/255))
.cornerRadius(10)
}
Spacer()
}}
Since I was stuck on the same thing. Here is a quick solution I found working.
import StoreKit
import SwiftUI
import UIKit
struct StoreView: UIViewControllerRepresentable {
var dismissHandler: () -> Void
func makeUIViewController(context: UIViewControllerRepresentableContext<StoreView>) -> StoreViewController {
return StoreViewController(coordinator: context.coordinator)
}
func updateUIViewController(_ uiViewController: StoreViewController, context: UIViewControllerRepresentableContext<StoreView>) {
}
public func makeCoordinator() -> StoreViewCoordinator {
.init(dismissHandler: dismissHandler)
}
}
class StoreViewController: UIViewController {
let coordinator: StoreViewCoordinator
var storeController: SKStoreProductViewController?
init(coordinator: StoreViewCoordinator) {
self.coordinator = coordinator
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
storeController = SKStoreProductViewController()
storeController?.delegate = coordinator
storeController?.loadProduct(
withParameters: [SKStoreProductParameterITunesItemIdentifier: ******]
)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let storeController = storeController else {
return
}
present(storeController, animated: true)
}
}
class StoreViewCoordinator: NSObject, SKStoreProductViewControllerDelegate {
private let dismissHandler: () -> Void
init(dismissHandler: #escaping () -> Void) {
self.dismissHandler = dismissHandler
}
func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {
dismissHandler()
}
}
and then I am using it inside ZStack like:
StoreView(
dismissHandler: { viewStore.send(.setShowingStore(false)) }
)
.isHidden(!viewStore.isShowingStore, remove: true)
I am using TCA, so setting a property will be different in your case

SwiftUI - CNContactViewController NavigationBar problem

I am trying to implement CNContactViewDelegate to be able to show detail of the CNContact. And apparently, I am the first one to implement it with SwiftUI and getting problems. Anyway, I can see the detail of CNContact with using UIViewControllerRepresentable but I have an issue with the NavigationBar, which there is gap between the Contact's image and StatusBar -because of the NavigationBar and NavigationLink I think- and this gap is not there in the native Contacts app and apparently in this link that implemented the framework in UIKit.
Here is the code;
struct ContactsListView: View {
#ObservedObject var contactsModel: ContactsViewModel
var body: some View {
NavigationView{
List {
//After some ForEach's and Section's
//This view is working.
NavigationLink(destination: ContactDetailView(contact: self.$contactsModel.contacts[sectionIdx].contacts[contactIdx])) {
Text(self.contactsModel.contacts[sectionIdx].contacts[contactIdx].givenName)
}
}
.navigationBarTitle("Contacts")
}
}
}
struct ContactView: UIViewControllerRepresentable {
#Binding var contact: CNContact
func makeCoordinator() -> ContactView.Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ContactView>) -> CNContactViewController {
let controller = CNContactViewController(for: contact)
self.navigationBarHidden(true)
return controller
}
func updateUIViewController(_ uiViewController: CNContactViewController, context: UIViewControllerRepresentableContext<ContactView>) {
print(context)
}
class Coordinator: NSObject, CNContactViewControllerDelegate {
var parent: ContactView
init(_ contactDetail: ContactView) {
self.parent = contactDetail
self.parent.navigationBarHidden(true)
}
}
}
In the ContactView, both of those self.navigationBarHidden(true)'s are not working. As an example of the problem here is the native app's screenshot;
And here is the result of my code;
Posted my comment on the solution and then I came to the idea to wrap the contact view controller inside my custom NavigationController. And voila that fixed it!
struct ContactView: UIViewControllerRepresentable {
var contact: CNContact
func makeCoordinator() -> ContactView.Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ContactView>) -> NavigationController {
let controller = CNContactViewController(forUnknownContact: contact)
controller.contactStore = CNContactStore()
controller.delegate = context.coordinator
let navigationController = NavigationController(rootViewController: controller)
return navigationController
}
func updateUIViewController(_ uiViewController: NavigationController, context: UIViewControllerRepresentableContext<ContactView>) {
}
class Coordinator: NSObject, CNContactViewControllerDelegate {
var parent: ContactView
init(_ contactDetail: ContactView) {
self.parent = contactDetail
}
func contactViewController(_ viewController: CNContactViewController,
didCompleteWith contact: CNContact?) {
}
func contactViewController(_ viewController: CNContactViewController,
shouldPerformDefaultActionFor property: CNContactProperty) -> Bool {
return true
}
}
}
As the question is got an upvote I thought I can share my half way solution. This solves the gap however during the transition to detail there is a glitch of navigation bar with background color. After the transition it is becoming clear.
struct ContactDetailView: View {
var contact: CNContact
var body: some View {
ZStack {
Color.clear
ContactView(contact: self.contact)
.navigationBarTitle("", displayMode: .inline)
}.edgesIgnoringSafeArea(.top)
}
}
struct ContactView: UIViewControllerRepresentable {
var contact: CNContact
func makeCoordinator() -> ContactView.Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ContactView>) -> CNContactViewController {
let controller = CNContactViewController(forUnknownContact: contact)
controller.allowsActions = true
controller.allowsEditing = false
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: CNContactViewController, context: UIViewControllerRepresentableContext<ContactView>) {
print("updated")
}
class Coordinator: NSObject, CNContactViewControllerDelegate {
var parent: ContactView
init(_ contactDetail: ContactView) {
self.parent = contactDetail
}
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
}
func contactViewController(_ viewController: CNContactViewController, shouldPerformDefaultActionFor property: CNContactProperty) -> Bool {
return true
}
}
}

UIViewControllerRepresentable and CNContactPickerViewController

Can't seem to create a UIViewControllerRepresentable that works with CNContactPickerViewController.
Using Xcode 11 beta 4, I've created number of other UIViewControllerRepresentable using other UIViewController and those have worked fine. I've tried changing the features of the CNContactPickerViewController and different implementations of the delegate.
import SwiftUI
import ContactsUI
// Minimal version
struct LookupContactVCR : UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> CNContactPickerViewController {
let contactPickerVC = CNContactPickerViewController()
contactPickerVC.delegate = context.coordinator
return contactPickerVC
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func updateUIViewController(_ uiViewController: CNContactPickerViewController, context: Context) {}
class Coordinator: NSObject {}
}
extension LookupContactVCR.Coordinator : CNContactPickerDelegate {
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
print("Chose: \(contact.givenName)")
}
}
#if DEBUG
struct LookupContact_Previews : PreviewProvider {
static var previews: some View {
LookupContactVCR()
}
}
#endif
No error messages. But the screen is always white with nothing rendered.
First of all, please file a [Bug Report][1] for this issue.
[1]: https://bugreport.apple.com
Secondly, there are 2 workarounds for this issue:
You can use ABPeoplePickerNavigationController which is deprecated but still works.
Create a UIViewController which presents CNContactPickerViewController on viewWillAppear and use this newly created view controller with SwiftUI.
1. ABPeoplePickerNavigationController
import SwiftUI
import AddressBookUI
struct PeoplePicker: UIViewControllerRepresentable {
typealias UIViewControllerType = ABPeoplePickerNavigationController
final class Coordinator: NSObject, ABPeoplePickerNavigationControllerDelegate, UINavigationControllerDelegate {
func peoplePickerNavigationController(_ peoplePicker: ABPeoplePickerNavigationController, didSelectPerson person: ABRecord) {
<#selected#>
}
func peoplePickerNavigationControllerDidCancel(_ peoplePicker: ABPeoplePickerNavigationController) {
<#cancelled#>
}
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIViewController(context: UIViewControllerRepresentableContext<PeoplePicker>) -> PeoplePicker.UIViewControllerType {
let result = UIViewControllerType()
result.delegate = context.coordinator
return result
}
func updateUIViewController(_ uiViewController: PeoplePicker.UIViewControllerType, context: UIViewControllerRepresentableContext<PeoplePicker>) { }
}
2. CNContactPickerViewController
EmbeddedContactPickerViewController
import Foundation
import ContactsUI
import Contacts
protocol EmbeddedContactPickerViewControllerDelegate: AnyObject {
func embeddedContactPickerViewControllerDidCancel(_ viewController: EmbeddedContactPickerViewController)
func embeddedContactPickerViewController(_ viewController: EmbeddedContactPickerViewController, didSelect contact: CNContact)
}
class EmbeddedContactPickerViewController: UIViewController, CNContactPickerDelegate {
weak var delegate: EmbeddedContactPickerViewControllerDelegate?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.open(animated: animated)
}
private func open(animated: Bool) {
let viewController = CNContactPickerViewController()
viewController.delegate = self
self.present(viewController, animated: false)
}
func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
self.dismiss(animated: false) {
self.delegate?.embeddedContactPickerViewControllerDidCancel(self)
}
}
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
self.dismiss(animated: false) {
self.delegate?.embeddedContactPickerViewController(self, didSelect: contact)
}
}
}
EmbeddedContactPicker
import SwiftUI
import Contacts
import Combine
struct EmbeddedContactPicker: UIViewControllerRepresentable {
typealias UIViewControllerType = EmbeddedContactPickerViewController
final class Coordinator: NSObject, EmbeddedContactPickerViewControllerDelegate {
func embeddedContactPickerViewController(_ viewController: EmbeddedContactPickerViewController, didSelect contact: CNContact) {
<#selected#>
}
func embeddedContactPickerViewControllerDidCancel(_ viewController: EmbeddedContactPickerViewController) {
<#cancelled#>
}
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIViewController(context: UIViewControllerRepresentableContext<EmbeddedContactPicker>) -> EmbeddedContactPicker.UIViewControllerType {
let result = EmbeddedContactPicker.UIViewControllerType()
result.delegate = context.coordinator
return result
}
func updateUIViewController(_ uiViewController: EmbeddedContactPicker.UIViewControllerType, context: UIViewControllerRepresentableContext<EmbeddedContactPicker>) { }
}
What I did, is just wrapping it inside a NavigationController. Maybe not as clean as arturigor's answer, but works quite easily.
func makeUIViewController(context: Context) -> some UIViewController {
// needs to be wrapper in another controller. Else isn't displayed
let navController = UINavigationController()
let controller = CNContactPickerViewController()
controller.delegate = delegate
controller.predicateForEnablingContact = enablingPredicate
navController.present(controller, animated: false, completion: nil)
return navController
}
Regarding the questions, how it should be displayed. I Just have it displayed conditionally as a view inside a group
Group {
Text("Sharing is caring")
if showContactPicker {
ContactPicker(contactType: .email)
}
}
import SwiftUI
import Contacts
import ContactsUI
struct SomeView: View {
#State var contact: CNContact?
var body: some View {
VStack {
Text("Selected: \(contact?.givenName ?? "")")
ContactPickerButton(contact: $contact) {
Label("Select Contact", systemImage: "person.crop.circle.fill")
.fixedSize()
}
.fixedSize()
.buttonStyle(.borderedProminent)
}
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView()
}
}
public struct ContactPickerButton<Label: View>: UIViewControllerRepresentable {
public class Coordinator: NSObject, CNContactPickerDelegate {
var onCancel: () -> Void
var viewController: UIViewController = .init()
var picker = CNContactPickerViewController()
#Binding var contact: CNContact?
// Possible take a binding
public init<Label: View>(contact: Binding<CNContact?>, onCancel: #escaping () -> Void, #ViewBuilder content: #escaping () -> Label) {
self._contact = contact
self.onCancel = onCancel
super.init()
let button = Button<Label>(action: showContactPicker, label: content)
let hostingController: UIHostingController<Button<Label>> = UIHostingController(rootView: button)
hostingController.view?.sizeToFit()
(hostingController.view?.frame).map {
hostingController.view!.widthAnchor.constraint(equalToConstant: $0.width).isActive = true
hostingController.view!.heightAnchor.constraint(equalToConstant: $0.height).isActive = true
viewController.preferredContentSize = $0.size
}
hostingController.willMove(toParent: viewController)
viewController.addChild(hostingController)
viewController.view.addSubview(hostingController.view)
hostingController.view.anchor(to: viewController.view)
picker.delegate = self
}
func showContactPicker() {
viewController.present(picker, animated: true)
}
public func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
onCancel()
}
public func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
self.contact = contact
}
func makeUIViewController() -> UIViewController {
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ContactPickerButton>) {
}
}
#Binding var contact: CNContact?
#ViewBuilder
var content: () -> Label
var onCancel: () -> Void
public static func defaultContent() -> SwiftUI.Label<Text, Image> {
SwiftUI.Label("Select Contact", systemImage: "person.crop.circle.fill")
}
public init(contact: Binding<CNContact?>, onCancel: #escaping () -> () = {}, #ViewBuilder content: #escaping () -> Label) {
self._contact = contact
self.onCancel = onCancel
self.content = content
}
public func makeCoordinator() -> Coordinator {
.init(contact: $contact, onCancel: onCancel, content: content)
}
public func makeUIViewController(context: Context) -> UIViewController {
context.coordinator.makeUIViewController()
}
public func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
context.coordinator.updateUIViewController(uiViewController, context: context)
}
}
fileprivate extension UIView {
func anchor(to other: UIView) {
self.translatesAutoresizingMaskIntoConstraints = false
self.topAnchor.constraint(equalTo: other.topAnchor).isActive = true
self.bottomAnchor.constraint(equalTo: other.bottomAnchor).isActive = true
self.leadingAnchor.constraint(equalTo: other.leadingAnchor).isActive = true
self.trailingAnchor.constraint(equalTo: other.trailingAnchor).isActive = true
}
}
The #youjin solution have an issue when you use it inside a Sheet with navigationView.
For example, first I present an .sheet view, inside this sheet view I have and NavigationView as child, then, inside all this, I present the Contact Picker. For this scenario when Contact Picker dismiss, also dismiss my sheet view parent.
I added an #Environment(\.presentationMode) variable and I dismissed using the Coordinator approach. Look my solution here:
import SwiftUI
import ContactsUI
/**
Presents a CNContactPickerViewController view modally.
- Parameters:
- showPicker: Binding variable for presenting / dismissing the picker VC
- onSelectContact: Use this callback for single contact selection
- onSelectContacts: Use this callback for multiple contact selections
*/
public struct ContactPicker: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var showPicker: Bool
#State private var viewModel = ContactPickerViewModel()
public var onSelectContact: ((_: CNContact) -> Void)?
public var onSelectContacts: ((_: [CNContact]) -> Void)?
public var onCancel: (() -> Void)?
public init(showPicker: Binding<Bool>, onSelectContact: ((_: CNContact) -> Void)? = nil, onSelectContacts: ((_: [CNContact]) -> Void)? = nil, onCancel: (() -> Void)? = nil) {
self._showPicker = showPicker
self.onSelectContact = onSelectContact
self.onSelectContacts = onSelectContacts
self.onCancel = onCancel
}
public func makeUIViewController(context: UIViewControllerRepresentableContext<ContactPicker>) -> ContactPicker.UIViewControllerType {
let dummy = _DummyViewController()
viewModel.dummy = dummy
return dummy
}
public func updateUIViewController(_ uiViewController: _DummyViewController, context: UIViewControllerRepresentableContext<ContactPicker>) {
guard viewModel.dummy != nil else {
return
}
// able to present when
// 1. no current presented view
// 2. current presented view is being dismissed
let ableToPresent = viewModel.dummy.presentedViewController == nil || viewModel.dummy.presentedViewController?.isBeingDismissed == true
// able to dismiss when
// 1. cncpvc is presented
let ableToDismiss = viewModel.vc != nil
if showPicker && viewModel.vc == nil && ableToPresent {
let pickerVC = CNContactPickerViewController()
pickerVC.delegate = context.coordinator
viewModel.vc = pickerVC
viewModel.dummy.present(pickerVC, animated: true)
} else if !showPicker && ableToDismiss {
// viewModel.dummy.dismiss(animated: true)
self.viewModel.vc = nil
}
}
public func makeCoordinator() -> CNContactPickerDelegate {
if self.onSelectContacts != nil {
return MultipleSelectionCoordinator(self)
} else {
return SingleSelectionCoordinator(self)
}
}
public final class SingleSelectionCoordinator: NSObject, CNContactPickerDelegate {
var parent : ContactPicker
init(_ parent: ContactPicker){
self.parent = parent
}
public func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
parent.showPicker = false
parent.onCancel?()
}
public func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
parent.showPicker = false
parent.onSelectContact?(contact)
}
}
public final class MultipleSelectionCoordinator: NSObject, CNContactPickerDelegate {
var parent : ContactPicker
init(_ parent: ContactPicker){
self.parent = parent
}
public func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
parent.showPicker = false
parent.onCancel?()
parent.presentationMode.wrappedValue.dismiss()
}
public func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
parent.showPicker = false
parent.onSelectContacts?(contacts)
parent.presentationMode.wrappedValue.dismiss()
}
}
}
class ContactPickerViewModel {
var dummy: _DummyViewController!
var vc: CNContactPickerViewController?
}
//Don't use it any more 😐
//public protocol Coordinator: CNContactPickerDelegate {}
public class _DummyViewController: UIViewController {}
UPDATE
We only replace the Coordinator protocol by the CNContactPickerDelegate, and this way we avoid the error that Xcode show us.
"Inheritance from non-protocol, non-class type 'ContactPicker.Coordinator' (aka 'any Coordinator')."
A similar workaround
Please see below for a similar workaround that perhaps offers more flexibility around the delegate and event handling.
import SwiftUI
import ContactsUI
/// `UIViewRepresentable` to port `CNContactPickerViewController` for use with SwiftUI.
struct ContactPicker: UIViewControllerRepresentable {
#Binding var delegate: ContactPickerDelegate
public var displayedPropertyKeys: [String]?
// Sadly, we need to present the `CNContactPickerViewController` from another `UIViewController`.
// This is due to a confirmed bug -- see https://openradar.appspot.com/7103187.
class Presenter: UIViewController {}
public var presenter = Presenter()
typealias UIViewControllerType = Presenter
func makeUIViewController(context: Context) -> UIViewControllerType {
let picker = CNContactPickerViewController()
picker.delegate = delegate
picker.displayedPropertyKeys = displayedPropertyKeys
presenter.present(picker, animated: true)
return presenter
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
if !delegate.showPicker {
presenter.dismiss(animated: true)
}
}
}
/// Delegate required by `ContactPicker` to handle `CNContactPickerViewController` events.
/// Extend `ContactPickerDelegate` and implement/override its methods to provide custom functionality as required.
/// Listen/subscribe to `showPicker` in a `View` or `UIViewController`, e.g. to control whether `CNContactPickerViewController` is presented.
class ContactPickerDelegate: NSObject, CNContactPickerDelegate, ObservableObject {
#Published var showPicker: Bool = false
func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
self.showPicker = false
}
}
Example of usage in a SwiftUI View
import SwiftUI
import ContactsUI
struct ContactPickerView: View {
#ObservedObject var delegate = Delegate()
var body: some View {
VStack {
Text("Hi")
Button(action: {
delegate.showPicker = true
}, label: {
Text("Pick contact")
})
.sheet(isPresented: $delegate.showPicker, onDismiss: {
delegate.showPicker = false
}) {
ContactPicker(delegate: .constant(delegate))
}
if let contact = delegate.contact {
Text("Selected: \(contact.givenName)")
}
}
}
/// Provides `CNContactPickerDelegate` functionality tailored to this view's requirements.
class Delegate: ContactPickerDelegate {
#Published var contact: CNContact? = nil
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
print(contact.givenName)
self.contact = contact
self.showPicker = false
}
}
}
struct ContactPickerView_Previews: PreviewProvider {
static var previews: some View {
ContactPickerView()
}
}
Remarks
Unfortunately, this workaround suffers from the same issue where a blank white/gray screen (the additional UIViewController) is shown temporarily after the picker is dismissed.