Send Message to Apple Watch using WCSession in SwiftUI - swiftui

I did a exmaple long time ago how to send a simple message from an iPhone to a Apple Watch using Swift:
import UIKit
import WatchConnectivity
class ViewController: UIViewController, WCSessionDelegate {
// MARK: Outlets
#IBOutlet weak var textField: UITextField!
// MARK: Variables
var wcSession : WCSession! = nil
// MARK: Overrides
override func viewDidLoad() {
super.viewDidLoad()
wcSession = WCSession.default
wcSession.delegate = self
wcSession.activate()
}
// MARK: Button Actions
#IBAction func sendText(_ sender: Any) {
let txt = textField.text!
let message = ["message":txt]
wcSession.sendMessage(message, replyHandler: nil) { (error) in
print(error.localizedDescription)
}
}
// MARK: WCSession Methods
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
// Code
}
func sessionDidBecomeInactive(_ session: WCSession) {
// Code
}
func sessionDidDeactivate(_ session: WCSession) {
// Code
}
}
Now I'm trying to do the same using SwiftUI but no success so far.
Can anyone help with this problem?
I just need to know how to use the WCSession Class and the WCSessionDelegate with SwiftUI.
Thanks

I just had the same question as you and I figured it out:
First you need to implement a class that conforms to WCSessionDelegate. I like to use a separate class for that:
import WatchConnectivity
class ConnectivityProvider: NSObject, WCSessionDelegate {
private let session: WCSession
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
}
func send(message: [String:Any]) -> Void {
session.sendMessage(message, replyHandler: nil) { (error) in
print(error.localizedDescription)
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
// code
}
func sessionDidBecomeInactive(_ session: WCSession) {
// code
}
func sessionDidDeactivate(_ session: WCSession) {
// code
}
}
Now you need a ViewModel that takes your ConnectivityProvider as an argument. The ViewModel will be responsible for the connection of your View and the ConnectivityProvider. It also holds the value for the Textfield that later gets defined inside your View.
import SwiftUI
final class ViewModel: ObservableObject {
private(set) var connectivityProvider: ConnectivityProvider
var textFieldValue: String = ""
init(connectivityProvider: ConnectivityProvider) {
self.connectivityProvider = connectivityProvider
}
func sendMessage() -> Void {
let txt = textFieldValue
let message = ["message":txt]
connectivityProvider.send(message: message)
}
}
Now you can build a simple View that consists of a Textfield and a Button. Your View will be dependent on your ViewModel that you just defined.
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
TextField("Message Content", text: $viewModel.textFieldValue)
Button(action: {
self.viewModel.sendMessage()
}) {
Text("Send Message")
}
}
}
}
Last but not least you need to combine your ConnectivityProvider, ViewModel and View inside of your SceneDelegate:
let viewModel = ViewModel(connectivityProvider: ConnectivityProvider())
let contentView = ContentView(viewModel: viewModel)
...
window.rootViewController = UIHostingController(rootView: contentView)
==================================
Update: How to activate the Session?
First add a new function to your ConnectivityProvider that activates the session:
class ConnectivityProvider: NSObject, WCSessionDelegate {
...
func connect() {
guard WCSession.isSupported() else {
print("WCSession is not supported")
return
}
session.activate()
}
...
}
Now you can call the connect function whenever you need your WCSession to be connected. You should be able to connect it everywhere, like in your SceneDelegate, inside your ViewModel, or even directly inside of the init of your ConnectivityProvider:
ConnectivityProvider init:
class ConnectivityProvider: NSObject, WCSessionDelegate {
private let session: WCSession
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
self.connect()
}
...
}
ViewModel:
import SwiftUI
final class ViewModel: ObservableObject {
private(set) var connectivityProvider: ConnectivityProvider
var textFieldValue: String = ""
init(connectivityProvider: ConnectivityProvider) {
self.connectivityProvider = connectivityProvider
self.connectivityProvider.connect()
}
func sendMessage() -> Void {
let txt = textFieldValue
let message = ["message":txt]
connectivityProvider.send(message: message)
}
}
SceneDelegate:
let connectivityProvider = ConnectivityProvider()
connectivityProvider.connect()
let viewModel = ViewModel(connectivityProvider: connectivityProvider)
let contentView = ContentView(viewModel: viewModel)
...
window.rootViewController = UIHostingController(rootView: contentView)

Related

Set an #EnvrionmentVariable from within an ObservableObject helper class

I am using CoreBluetooth in SwiftUI and want to set the CBPeripheral use it throughout the app. Using a wrapper class as an EnvironmentObject seems like the right way to go, however I am not sure how to set it from within the ViewModel.
class BluetoothViewModel: NSObject, ObservableObject {
private var centralManager: CBCentralManager?
#Published var peripherals: [CBPeripheral] = []
override init() {
super.init()
self.centralManager = CBCentralManager(delegate: self, queue: .main)
self.centralManager?.delegate = self
}
func connectToDevice(btPeripheral: CBPeripheral) -> Void {
print("trying to connect to \(btPeripheral.name ?? "unknown")")
centralManager?.connect(btPeripheral, options: nil)
}
func disconnectDevice(btPeripheral: CBPeripheral) -> Void {
print("trying to disconnect to \(btPeripheral.name ?? "unknown")")
centralManager?.cancelPeripheralConnection(btPeripheral)
// WOULD LIKE TO SET THE #EnvrionmentObject TO nil HERE
}
}
...and after the connection:
extension BluetoothViewModel: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
print("powered on")
self.centralManager?.scanForPeripherals(withServices: nil)
} else {
//need to put an alert here to check bluetooth
print("powered off")
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if !peripherals.contains(peripheral) {
self.peripherals.append(peripheral)
}
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("connected to \(peripheral.name ?? "unknown")")
connectedPeripheral.peripheral = peripheral
// WOULD LIKE TO SET THE #EnvrionmentObject here
}
}
This way in the view I show the Bluetooth device
#EnvironmentObject var connectedPeripheral: ConnectedPeripheral
#ObservedObject private var bluetoothViewModel = BluetoothViewModel()
var body: some View {
if (connectedPeripheral.peripheral == nil) {
BluetoothDevicesView(). //this is where I list the devices and allow for selection
} else {
Text("We have one")
Button("Disconnect") {
print("pressed")
// DISCONNECT DEVICE HERE
bluetoothViewModel(connectedPeripheral.peripheral)
}
.buttonStyle(.borderedProminent)
}
}
The wrapper class would be basic
import SwiftUI
import CoreBluetooth
#MainActor class ConnectedPeripheral: ObservableObject {
#Published var peripheral: CBPeripheral? = nil
}
I didn't think having the ViewModel as an EnvironmentVariable would be the right way to go and I need to use the CBPerpheral in other views as well. I also would probably just turn the ViewModel into some sort of helper class.
I was also thinking of potentially setting the #EnvironmentVariable by having a #Published object in the BluetoothViewModel, but would not know how to watch for it to change. I'm open to suggestions.

SwiftUI MessageUI messageComposeViewController ever called

Looking to send SMS from a swiftui enviro .Passing recipients from a swiftui contentview, i can send sms . However, messageComposeViewController is never called hence modal is never dismissed and i dont get any callBack (same issue when trying to cancel)
import UIKit
import MessageUI
import SwiftUI
struct sms: View {
#State private var isShowingMessage = false
#State private var recipients = ["007"]
#State private var message = "this is my message "
#State private var resultValue = ""
var body: some View {
Button{
self.isShowingMessage.toggle()
} label : {
Text("Show messages")
}
.sheet(isPresented: $isShowingMessage) {
SMSMessageUIView(recipients:recipients,body: message,completion:handleCompletion(_:))
}
}
func handleCompletion (_ result:MessageComposeResult){
switch result {
case.cancelled :
resultValue = "cancelled"
break
case .sent :
resultValue = "sent"
break
case.failed :
resultValue = "failed"
break
#unknown default :
break
}
}
}
class SMSMessageViewController : UIViewController, MFMessageComposeViewControllerDelegate {
var delegate : MessagesViewDelegate?
var recipients : [String]?
var body :String?
override func viewDidLoad() {
super.viewDidLoad()
}
func displayMessageInterface(){
let composeVC = MFMessageComposeViewController()
composeVC.body = body ?? ""
composeVC.recipients = recipients
if MFMessageComposeViewController.canSendText(){
self.present(composeVC,animated:true,completion: nil)
}else{
self.delegate?.messageCompletion(result: MessageComposeResult.failed)
}
}
func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
print("NEVR CALLED")
self.delegate?.messageCompletion(result: result)
controller.dismiss(animated: true, completion: nil)
}
}
public protocol MessagesViewDelegate {
func messageCompletion(result: MessageComposeResult)
}
struct SMSMessageUIView : UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
let recipients: [String]?
let body : String
var completion : ((_ result: MessageComposeResult)-> Void)
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> SMSMessageViewController {
let controller = SMSMessageViewController()
controller.delegate = context.coordinator
controller.recipients = recipients
controller.body = body
return controller
}
func updateUIViewController(_ uiViewController: SMSMessageViewController, context: Context) {
uiViewController.recipients = recipients
uiViewController.displayMessageInterface()
}
class Coordinator :NSObject,UINavigationControllerDelegate,MessagesViewDelegate{
var parent:SMSMessageUIView
init(_ parent:SMSMessageUIView) {
self.parent = parent
}
func messageCompletion(result: MessageComposeResult) {
self.parent.presentationMode.wrappedValue.dismiss()
self.parent.completion(result)
}
}
}

How to access to SwiftUI content view in extension delegate on Apple Watch?

I need to call loadData in my ContentView when the app becomes active. ExtensionDelegate is a class which handle app events such as applicationDidBecomeActive. But I don't understand how to get ContentView inside ExtensionDelegate.
This is my ContentView:
struct ContentView: View {
let network = Network()
#State private var currentIndex: Int = 0
#State private var sources: [Source] = []
var body: some View {
ZStack {
// Some view depends on 'sources'
}
.onAppear(perform: loadData)
}
func loadData() {
network.getSources { response in
switch response {
case .result(let result):
self.sources = result.results
case .error(let error):
print(error)
}
}
}
}
And ExtensionDelegate:
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
}
func applicationDidBecomeActive() {
// Here I need to call 'loadData' of my ContentView
}
func applicationWillResignActive() {
}
...
The simplest solution as I see would be to use notification
in ContentView
let needsReloadNotification = NotificationCenter.default.publisher(for: .needsNetworkReload)
var body: some View {
ZStack {
// Some view depends on 'sources'
}
.onAppear(perform: loadData)
.onReceive(needsReloadNotification) { _ in self.loadData()}
}
and in ExtensionDelegate
func applicationDidBecomeActive() {
NotificationCenter.default.post(name: .needsNetworkReload, object: nil)
}
and somewhere in shared
extension Notification.Name {
static let needsNetworkReload = Notification.Name("NeedsReload")
}

Google place autocomplete API with SwiftUI

I am trying to implement auto search for address in my App using SwiftUI. Google tutorial are based on UIKit, I was hoping if someone has tried and can guide me to the right direction. Thanks
Try this code; it works for me.
import Foundation
import UIKit
import SwiftUI
import GooglePlaces
struct PlacePicker: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
#Environment(\.presentationMode) var presentationMode
#Binding var address: String
func makeUIViewController(context: UIViewControllerRepresentableContext<PlacePicker>) -> GMSAutocompleteViewController {
let autocompleteController = GMSAutocompleteViewController()
autocompleteController.delegate = context.coordinator
let fields: GMSPlaceField = GMSPlaceField(rawValue: UInt(GMSPlaceField.name.rawValue) |
UInt(GMSPlaceField.placeID.rawValue))!
autocompleteController.placeFields = fields
let filter = GMSAutocompleteFilter()
filter.type = .address
filter.country = "ZA"
autocompleteController.autocompleteFilter = filter
return autocompleteController
}
func updateUIViewController(_ uiViewController: GMSAutocompleteViewController, context: UIViewControllerRepresentableContext<PlacePicker>) {
}
class Coordinator: NSObject, UINavigationControllerDelegate, GMSAutocompleteViewControllerDelegate {
var parent: PlacePicker
init(_ parent: PlacePicker) {
self.parent = parent
}
func viewController(_ viewController: GMSAutocompleteViewController, didAutocompleteWith place: GMSPlace) {
DispatchQueue.main.async {
print(place.description.description as Any)
self.parent.address = place.name!
self.parent.presentationMode.wrappedValue.dismiss()
}
}
func viewController(_ viewController: GMSAutocompleteViewController, didFailAutocompleteWithError error: Error) {
print("Error: ", error.localizedDescription)
}
func wasCancelled(_ viewController: GMSAutocompleteViewController) {
parent.presentationMode.wrappedValue.dismiss()
}
}
}

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.