SwiftUI - CNContactViewController NavigationBar problem - swiftui

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
}
}
}

Related

Can't drag down/dismiss UIColorPickerViewController

I am displaying a UIColorPickerViewController as a sheet using the sheet() method, everything works fine but I can't drag down/dismiss the view anymore.
import Foundation
import SwiftUI
struct ColorPickerView: UIViewControllerRepresentable {
private var selectedColor: UIColor!
init(selectedColor: UIColor) {
self.selectedColor = selectedColor
}
func makeUIViewController(context: Context) -> UIColorPickerViewController {
let colorPicker = UIColorPickerViewController()
colorPicker.selectedColor = self.selectedColor
return colorPicker
}
func updateUIViewController(_ uiViewController: UIColorPickerViewController, context: Context) {
// Silent
}
}
.sheet(isPresented: self.$viewManager.showSheet, onDismiss: {
ColorPickerView()
}
Any idea how to make the drag/down dismiss gesture works?
Thanks!
Ran into the same problem when trying to build a color picker similar to above. What worked was "wrapping" the color picker in a view with a Dismiss button. And also discovered that the bar at the top of the view would allow the picker to now be dragged down and away. Below is my wrapper. (One could add more features such as a title to the bar.)
struct ColorWrapper: View {
var inputColor: UIColor
#Binding var isShowingColorPicker: Bool
#Binding var selectedColor: UIColor?
var body: some View {
VStack {
HStack {
Spacer()
Button("Dismiss", action: {
isShowingColorPicker = false
}).padding()
}
ColorPickerView(inputColor: inputColor, selectedColor: $selectedColor)
}
}
}
And for completeness, here is my version of the color picker:
import SwiftUI
struct ColorPickerView: UIViewControllerRepresentable {
typealias UIViewControllerType = UIColorPickerViewController
var inputColor: UIColor
#Binding var selectedColor: UIColor?
#Environment(\.presentationMode) var isPresented
func makeUIViewController(context: Context) -> UIColorPickerViewController {
let picker = UIColorPickerViewController()
picker.delegate = context.coordinator
picker.supportsAlpha = false
picker.selectedColor = inputColor
return picker
}
func updateUIViewController(_ uiViewController: UIColorPickerViewController, context: Context) {
uiViewController.supportsAlpha = false
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIColorPickerViewControllerDelegate {
var parent: ColorPickerView
init(parent: ColorPickerView) {
self.parent = parent
}
func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) {
parent.isPresented.wrappedValue.dismiss()
}
func colorPickerViewController(_ viewController: UIColorPickerViewController, didSelect color: UIColor, continuously: Bool) {
parent.selectedColor = color
// parent.isPresented.wrappedValue.dismiss()
}
}
}

SwiftUI : QuickLook doesn't not work correctly in iPad device

I tried to using QuickLook framework.
For editting PDF, I implemented "previewController(_: editingModeFor: )" in Coordinator.
In Xcode(ver 11.6) simulator, quicklook view has pencil markup tool.
In Xcode simulator
But in my iPad(PadOS 13.6), there is no markup tool.
In my iPad device, PadOS 13.6
Is there any bugs in QuickLook framework?
Here is my code.
PreviewController.swift
import SwiftUI
import QuickLook
struct PreviewController: UIViewControllerRepresentable {
let url: URL
#Binding var isPresented: Bool
func makeUIViewController(context: Context) -> UINavigationController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
controller.delegate = context.coordinator
let navigationController = UINavigationController(rootViewController: controller)
return navigationController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, QLPreviewControllerDelegate, QLPreviewControllerDataSource {
let parent: PreviewController
init(parent: PreviewController) {
self.parent = parent
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return parent.url as NSURL
}
/*
Return .updateContents so QLPreviewController takes care of updating the contents of the provided QLPreviewItems whenever users save changes.
*/
func previewController(_ controller: QLPreviewController, editingModeFor previewItem: QLPreviewItem) -> QLPreviewItemEditingMode {
return .updateContents
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
let fileUrl = Bundle.main.url(forResource: "LoremIpsum", withExtension: "pdf")!
#State private var showingPreview = false
var body: some View {
Button("Preview File") {
self.showingPreview = true
}
.sheet(isPresented: $showingPreview) {
PreviewController(url: self.fileUrl, isPresented: self.$showingPreview)
}
}
}

Implement PencilKit undo functionality using SwiftUI

Edit: Thanks to some of the feedback, I have been able to get this partially working (updated code to reflect current changes).
Even though the app appears to be working as intended, I am still getting the 'Modifying state...' warning. How can I update the view's drawing in updateUIView and push new drawings onto the stack with the canvasViewDrawingDidChange without causing this issue? I have tried wrapping it in a dispatch call, but that just creates an infinite loop.
I'm trying to implement undo functionality in a UIViewRepresentable (PKCanvasView). I have a parent SwiftUI view called WriterView which holds two buttons and the canvas.
Here's the parent view:
struct WriterView: View {
#State var drawings: [PKDrawing] = [PKDrawing()]
var body: some View {
VStack(spacing: 10) {
Button("Clear") {
self.drawings = []
}
Button("Undo") {
if !self.drawings.isEmpty {
self.drawings.removeLast()
}
}
MyCanvas(drawings: $drawings)
}
}
}
Here is how I've implemented my UIViewRepresentable:
struct MyCanvas: UIViewRepresentable {
#Binding var drawings: [PKDrawing]
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.delegate = context.coordinator
return canvas
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
uiView.drawing = self.drawings.last ?? PKDrawing()
}
func makeCoordinator() -> Coordinator {
Coordinator(self._drawings)
}
class Coordinator: NSObject, PKCanvasViewDelegate {
#Binding drawings: [PKDrawing]
init(_ drawings: Binding<[PKDrawing]>) {
self._drawings = drawings
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
drawings.append(canvasView.drawing)
}
}
}
I am getting the following error:
[SwiftUI] Modifying state during view update, this will cause undefined behavior.
Presumably it is being caused by my coordinator's did change function, but I'm not sure how to fix this. What is the best way to approach this?
Thanks!
I finally (accidentally) figured out how to do this using UndoManager. I'm still not sure exactly why this works, because I never have to call self.undoManager?.registerUndo(). Please comment if you understand why I never have to register an event.
Here's my working parent view:
struct Writer: View {
#Environment(\.undoManager) private var undoManager
#State private var canvasView = PKCanvasView()
var body: some View {
VStack(spacing: 10) {
Button("Clear") {
canvasView.drawing = PKDrawing()
}
Button("Undo") {
undoManager?.undo()
}
Button("Redo") {
undoManager?.redo()
}
MyCanvas(canvasView: $canvasView)
}
}
}
Here's my working child view:
struct MyCanvas: UIViewRepresentable {
#Binding var canvasView: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvasView.drawingPolicy = .anyInput
canvasView.tool = PKInkingTool(.pen, color: .black, width: 15)
return canvasView
}
func updateUIView(_ canvasView: PKCanvasView, context: Context) { }
}
This certainly feels more like the intended approach for SwiftUI and is certainly more elegant than the attempts I made earlier.
just for completeness and if you want to show the PKToolPicker, here is my UIViewRepresentable.
import Foundation
import SwiftUI
import PencilKit
struct PKCanvasSwiftUIView : UIViewRepresentable {
let canvasView = PKCanvasView()
#if !targetEnvironment(macCatalyst)
let coordinator = Coordinator()
class Coordinator: NSObject, PKToolPickerObserver {
// initial values
var color = UIColor.black
var thickness = CGFloat(30)
func toolPickerSelectedToolDidChange(_ toolPicker: PKToolPicker) {
if toolPicker.selectedTool is PKInkingTool {
let tool = toolPicker.selectedTool as! PKInkingTool
self.color = tool.color
self.thickness = tool.width
}
}
func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
if toolPicker.selectedTool is PKInkingTool {
let tool = toolPicker.selectedTool as! PKInkingTool
self.color = tool.color
self.thickness = tool.width
}
}
}
func makeCoordinator() -> PKCanvasSwiftUIView.Coordinator {
return Coordinator()
}
#endif
func makeUIView(context: Context) -> PKCanvasView {
canvasView.isOpaque = false
canvasView.backgroundColor = UIColor.clear
canvasView.becomeFirstResponder()
#if !targetEnvironment(macCatalyst)
if let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first,
let toolPicker = PKToolPicker.shared(for: window) {
toolPicker.addObserver(canvasView)
toolPicker.addObserver(coordinator)
toolPicker.setVisible(true, forFirstResponder: canvasView)
}
#endif
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
}
}
I think the error probably comes from the private func clearCanvas()
and private func undoDrawing(). Try this to see if it works:
private func clearCanvas() {
DispatchQueue.main.async {
self.drawings = [PKDrawing()]
}
}
Similarly for undoDrawing().
If it is from canvasViewDrawingDidChange, do same trick.
I have something working with this:
struct MyCanvas: UIViewRepresentable {
#Binding var drawings: [PKDrawing]
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.delegate = context.coordinator
return canvas
}
func updateUIView(_ canvas: PKCanvasView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self._drawings)
}
class Coordinator: NSObject, PKCanvasViewDelegate {
#Binding var drawings: [PKDrawing]
init(_ drawings: Binding<[PKDrawing]>) {
self._drawings = drawings
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
self.drawings.append(canvasView.drawing)
}
}
}
I think I have something working without the warning using a different approach.
struct ContentView: View {
let pkCntrl = PKCanvasController()
var body: some View {
VStack(spacing: 10) {
Button("Clear") {
self.pkCntrl.clear()
}
Spacer()
Button("Undo") {
self.pkCntrl.undoDrawing()
}
Spacer()
MyCanvas(cntrl: pkCntrl)
}
}
}
struct MyCanvas: UIViewRepresentable {
var cntrl: PKCanvasController
func makeUIView(context: Context) -> PKCanvasView {
cntrl.canvas = PKCanvasView()
cntrl.canvas.delegate = context.coordinator
cntrl.canvas.becomeFirstResponder()
return cntrl.canvas
}
func updateUIView(_ uiView: PKCanvasView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, PKCanvasViewDelegate {
var parent: MyCanvas
init(_ uiView: MyCanvas) {
self.parent = uiView
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
if !self.parent.cntrl.didRemove {
self.parent.cntrl.drawings.append(canvasView.drawing)
}
}
}
}
class PKCanvasController {
var canvas = PKCanvasView()
var drawings = [PKDrawing]()
var didRemove = false
func clear() {
canvas.drawing = PKDrawing()
drawings = [PKDrawing]()
}
func undoDrawing() {
if !drawings.isEmpty {
didRemove = true
drawings.removeLast()
canvas.drawing = drawings.last ?? PKDrawing()
didRemove = false
}
}
}

SwiftUI NavigationBar height

How to get current NavigationBar height? In UIKit we could get
navigationController?.navigationBar.frame.height
but can't find anything for SwiftUI...
Based on this post (thanks to Asperi): https://stackoverflow.com/a/59972635/12299030
struct NavBarAccessor: UIViewControllerRepresentable {
var callback: (UINavigationBar) -> Void
private let proxyController = ViewController()
func makeUIViewController(context: UIViewControllerRepresentableContext<NavBarAccessor>) ->
UIViewController {
proxyController.callback = callback
return proxyController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<NavBarAccessor>) {
}
typealias UIViewControllerType = UIViewController
private class ViewController: UIViewController {
var callback: (UINavigationBar) -> Void = { _ in }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let navBar = self.navigationController {
self.callback(navBar.navigationBar)
}
}
}
}
And then we can call this from any View:
.background(NavBarAccessor { navBar in
print(">> NavBar height: \(navBar.bounds.height)")
// !! use as needed, in calculations, #State, etc.
})
Building on #yoprst 's response, rather than configure calculations and #State variables, you could also return a View to insert directly into the hierarchy:
struct NavigationBarAccessor: UIViewControllerRepresentable {
var callback: (UINavigationBar) -> (AnyView)
private let proxyViewController = ProxyViewController()
func makeUIViewController(context: UIViewControllerRepresentableContext<NavigationBarAccessor>) -> UIViewController {
self.proxyViewController.callback = callback
return proxyViewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<NavigationBarAccessor>) {
}
private class ProxyViewController: UIViewController {
var callback: ((UINavigationBar) -> AnyView)?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let navigationBar = self.navigationController?.navigationBar {
_ = self.callback?(navigationBar)
}
}
}
}
Usage:
VStack {
NavigationBarAccessor { navigationBar in
Spacer()
.frame(height: navigationBar.frame.height)
}
}
Use GeometryReader, like this:
ZStack {
GeometryReader { geometry in
Rectangle().fill(Color.blue).frame(height: geometry.safeAreaInsets.bottom + 44.0)
}
Text("Content")
}
.edgesIgnoringSafeArea(.top)

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.