SwiftUI NavigationBar height - 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)

Related

SwiftUI: In SplitView, how can I detect if the master view is visible?

When SwiftUI creates a SplitView, it adds a toolbar button that hides/shows the Master view. How can I detect this change so that I can resize the font in the detail screen and use all the space optimally?
I've tried using .onChange with geometry but can't seem to get that to work.
If you're using iOS 16 you can use NavigationSplitView with NavigationSplitViewVisibility
Example:
struct MySplitView: View {
#State private var columnVisibility: NavigationSplitViewVisibility = .all
var bothAreShown: Bool { columnVisibility != .detailOnly }
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
Text("Master Column")
} detail: {
Text("Detail Column")
Text(bothAreShown ? "Both are shown" : "Just detail shown")
}
}
}
After thinkering for a while on this I got to this solution:
struct ContentView: View {
#State var isOpen = true
var body: some View {
NavigationView {
VStack{
Text("Primary")
.onUIKitAppear {
isOpen.toggle()
}
.onAppear{
print("hello")
isOpen.toggle()
}
.onDisappear{
isOpen.toggle()
print("hello: bye")
}
.navigationTitle("options")
}
Text("Secondary").font(isOpen ? .body : .title)
}.navigationViewStyle(.columns)
}
}
The onUIKitAppear is a custom extension suggested by apple to be only executed once the view has been presented to the user https://developer.apple.com/forums/thread/655338?page=2
struct UIKitAppear: UIViewControllerRepresentable {
let action: () -> Void
func makeUIViewController(context: Context) -> UIAppearViewController {
let vc = UIAppearViewController()
vc.delegate = context.coordinator
return vc
}
func makeCoordinator() -> Coordinator {
Coordinator(action: self.action)
}
func updateUIViewController(_ controller: UIAppearViewController, context: Context) {}
class Coordinator: ActionRepresentable {
var action: () -> Void
init(action: #escaping () -> Void) {
self.action = action
}
func remoteAction() {
action()
}
}
}
protocol ActionRepresentable: AnyObject {
func remoteAction()
}
class UIAppearViewController: UIViewController {
weak var delegate: ActionRepresentable?
var savedView: UIView?
override func viewDidLoad() {
self.savedView = UILabel()
if let _view = self.savedView {
view.addSubview(_view)
}
}
override func viewDidAppear(_ animated: Bool) {
delegate?.remoteAction()
}
override func viewDidDisappear(_ animated: Bool) {
view.removeFromSuperview()
savedView?.removeFromSuperview()
}
}
public extension View {
func onUIKitAppear(_ perform: #escaping () -> Void) -> some View {
self.background(UIKitAppear(action: perform))
}
}

UISearchController how to activate from SwiftUI

I found an integration of the uiSearchController in SwiftUI, but I don't know how to let it become active?
I found this:
I want that the searchBar becomes active when changing an Bool in the SwiftUI View with a #State for example.
If I add a Binding to the view modifier and set the isActive property of the searchController in
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationItem.hidesSearchBarWhenScrolling = false
}
then is doesn't become active.
Im not really familiar with UIKit, perhaps anybody knows how to correctly activate the searchbar that one can start typing for a search.
class SearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
}
}
extension SearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) {
}
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationItem.hidesSearchBarWhenScrolling = false
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: SearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
To activate a UISearchBar (which is what you're using), just do:
searchController.searchBar.becomeFirstResponder()
(from this answer)
Now all we need to do is reference searchController.searchBar from the SwiftUI view. First, add a function to your SearchBar class.
class SearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
}
/// add this function
func activate() {
searchController.searchBar.becomeFirstResponder()
}
}
Then, just call it. I think this is better than setting a #State, but if you require that, let me know and I'll edit my answer.
struct ContentView: View {
#StateObject var searchBar = SearchBar()
var body: some View {
NavigationView {
Button(action: {
searchBar.activate() /// activate the search bar
}) {
Text("Activate search bar")
}
.modifier(SearchBarModifier(searchBar: searchBar))
.navigationTitle("Navigation View")
}
}
}
Result:

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.