UIViewControllerRepresentable и CNContactPickerViewController

Невозможно создать UIViewControllerRepresentable, который работает с CNContactPickerViewController.

Используя Xcode 11 beta 4, я создал ряд других UIViewControllerRepresentable, используя другой UIViewController, и они работали нормально. Я попытался изменить функции CNContactPickerViewController и различные реализации делегата.

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

Нет сообщений об ошибках. Но экран всегда белый и ничего не отображается.

5 ответов

Прежде всего, пожалуйста, отправьте отчет об ошибке по этой проблеме. Во-вторых, есть 2 обходных пути для этой проблемы:

  1. Вы можете использовать ABPeoplePickerNavigationController который устарел, но все еще работает.
  2. Создать UIViewController какие подарки CNContactPickerViewController на viewWillAppear и использовать этот вновь созданный контроллер представления с 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

protocol EmbeddedContactPickerViewControllerDelegate: class {
    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>) { }

}

Я просто обернул его в NavigationController. Может быть, не такой чистый, как ответ Артуригора, но работает довольно легко.

      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
}

По поводу вопросов, как это должно отображаться. Я просто условно отобразил это как представление внутри группы

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

У решения @youjin есть проблема, когда вы используете его внутри Sheet с navigationView.

Например, сначала я представляю .sheetview, внутри этого представления листа у меня есть NavigationView как дочерний, а затем внутри всего этого я представляю средство выбора контактов . Для этого сценария, когда средство выбора контактов закрывает , также закройте родительский элемент представления листа.

Я добавил @Environment(\.presentationMode) переменная, и я отклонил ее, используя Coordinatorподход. Посмотрите мое решение здесь:

      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() -> Coordinator {
        if self.onSelectContacts != nil {
            return MultipleSelectionCoordinator(self)
        } else {
            return SingleSelectionCoordinator(self)
        }
    }
    
    public final class SingleSelectionCoordinator: NSObject, Coordinator {
        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, Coordinator {
        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?
}

public protocol Coordinator: CNContactPickerDelegate {}

public class _DummyViewController: UIViewController {}

Похожий способ обхода

См. Ниже аналогичный обходной путь, который, возможно, обеспечивает большую гибкость при обработке делегатов и событий.

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

Пример использования в 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()
    }
}

Замечания

К сожалению, этот обходной путь страдает той же проблемой, когда пустой белый / серый экран ( дополнительный UIViewController) отображается временно после закрытия сборщика.

Другие вопросы по тегам