SwiftUI: Open CNContactViewController from NavigationView? - swiftui

I'm trying to open CNContactViewController for creating a new contact from a NavigationView. So far this is what I've tried and failed:
Added a navigation bar item and set the destination
.navigationBarItems(trailing:
NavigationLink(destination: self.addContact()) {
Text("Add")
}
)
addContact function returns the new view
func addContact() -> CNContactViewController {
let con = CNContact()
let vc = CNContactViewController(forNewContact: con)
return vc
}
Unfortunately this doesn't seem to be working. I'm pretty new to iOS and SwiftUI and I'm not sure if this is the way to do it, any help would be appreciated!

You must embed CNContactViewController in a UIViewControllerRepresentable.
Here's the Apple documentation.
And, here's a non-Apple article about it.
Because you're new to iOS, please be aware that SwiftUI is far from finished and fully documented. And that SwiftUI only runs on the newest iOS 13, so not all your potential app users may (already) have that iOS version installed.

complete solution ;
.navigationBarItems(trailing:
NavigationLink(destination: self.addContact()) {
Text("Add")
}
)
function;
func addContact() -> MyCNContactViewController {
let vc = MyCNContactViewController()
return vc
}
represantable struct to wrap `CNContactViewController``
struct MyCNContactViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = CNContactViewController
func makeUIViewController(context: Context) -> CNContactViewController {
let con = CNContact()
let vc = CNContactViewController(forNewContact: con)
vc.allowsActions = true
return vc
}
func updateUIViewController(_ uiViewController: CNContactViewController, context: Context) {
}
}

Related

UIDocumentpicker Content are greyed out

I’m using UIViewControllerRepresentable for Document picker presentation in swiftUI. The issue is that I'm not able to select the video audio pdf it's all showing in a frozen manner. I need to fix this issues for the iOS 14 and above version. I'm able to select the file by tap-hold and then release its only works with the simulator in real devices it’s not possible for both the simulator and devices except for the file structure, the rest of the documents are in greyed out.
enter image description here
struct DocumentPicker: UIViewControllerRepresentable {
#ObservedObject var chatViewModel: RedesignChatViewModel
func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPicker>) -> UIDocumentPickerViewController {
let viewController = UIDocumentPickerViewController(forOpeningContentTypes: [.pdf, .mp3, .audio, .video, .movie, .item])
viewController.shouldShowFileExtensions = true
viewController.allowsMultipleSelection = false
viewController.delegate = context.coordinator
return viewController
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {
}
}
extension DocumentPicker {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIDocumentPickerDelegate {
var parent: DocumentPicker
var path: String?
init(_ documentPicker: DocumentPicker) {
self.parent = documentPicker
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else { return }
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
controller.dismiss(animated: true)
}
There are a few mistakes, first you'll need to create a UIViewController and use that to present the UIDocumentPickerViewController. Also you need to change Coordinator(self) to Coordinator() and in makeUIViewController return context.coordinator.myViewController that should be a lazy property. The reason for this is that self you are passing in is immediately out of date because it is a value type. You also need to remove the #ObservedObject and add lets or #Binding vars for your properties. When the repreresentable is init with new values for those properties, updateUIViewController will be called and you can then update the coordinator and view controller with the new values.

UIButton in SwiftUI Catalyst Mac app doesn't work when clicked second time

Here's the code I have:
private struct ShareButton: UIViewRepresentable {
func makeUIView(context: Context) -> UIButton {
let activityViewController = UIActivityViewController(activityItems: [URL(string: "https://www.apple.com/")!], applicationActivities: nil)
let action = UIAction(title: "Share") { _ in UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController?.present(activityViewController, animated: false) }
let button = UIButton(primaryAction: action)
activityViewController.popoverPresentationController?.sourceView = button
return button
}
func updateUIView(_ uiView: UIButton, context: Context) { }
}
Basically it's creating a UIButton with a UIAction, inside which there's a UIActivityViewController that set sourceView for the share menu to be the UIButton.
Here's a demo of the issue:
The UIButton is created when the SwiftUI view is created, and set as the sourceView. My guess is that the issue occur because the UIButton is somehow destroyed and recreated due to some SwiftUI mechanism? I can be entirely wrong though. Anyway to solve this?
Or any other way to do share button in a SwiftUI Catalyst Mac app?
"Or any other way to do share button in a SwiftUI Catalyst Mac app?"
You could try this approach, using the extension from:
How to get rid of message " 'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead" with AdMob banner?
public extension UIApplication {
func currentUIWindow() -> UIWindow? {
let connectedScenes = UIApplication.shared.connectedScenes
.filter({
$0.activationState == .foregroundActive})
.compactMap({$0 as? UIWindowScene})
let window = connectedScenes.first?
.windows
.first { $0.isKeyWindow }
return window
}
}
struct ContentView: View {
let holaTxt = "Hola 😀 "
var body: some View {
Button(action: {
let AV = UIActivityViewController(activityItems: [holaTxt], applicationActivities: nil)
UIApplication.shared.currentUIWindow()?.rootViewController?.present(AV, animated: true, completion: nil)
}) {
Text("Share")
}
}
}
Found an elegant solution for creating a share button in a SwiftUI Catalyst Mac app (in fact, all SwiftUI app), see https://github.com/SwiftUI-Plus/ActivityView

Display Share Sheet (UIActivityViewController) in a Modal .sheet() in SwiftUI [duplicate]

I'm trying to present a UIActivityViewController (share sheet) from a SwiftUI View. I created a view called ShareSheet conformed to UIViewControllerRepresentable to configure the UIActivityViewController, but it's turning out to be not as trivial to actually present this.
struct ShareSheet: UIViewControllerRepresentable {
typealias UIViewControllerType = UIActivityViewController
var sharing: [Any]
func makeUIViewController(context: UIViewControllerRepresentableContext<ShareSheet>) -> UIActivityViewController {
UIActivityViewController(activityItems: sharing, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ShareSheet>) {
}
}
Doing so naively via .sheet leads to the following.
.sheet(isPresented: $showShareSheet) {
ShareSheet(sharing: [URL(string: "https://example.com")!])
}
Is there a way to present this like it's usually presented? As in covering half the screen?
Hope this will help you,
struct ShareSheetView: View {
var body: some View {
Button(action: actionSheet) {
Image(systemName: "square.and.arrow.up")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 36, height: 36)
}
}
func actionSheet() {
guard let data = URL(string: "https://www.apple.com") else { return }
let av = UIActivityViewController(activityItems: [data], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
}
}
In iOS 14, Swift 5, Xcode 12.5 at least, I was able to accomplish this fairly easily by simply wrapping the UIActivityViewController in another view controller. It doesn't require inspecting the view hierarchy or using any 3rd party libraries. The only hackish part is asynchronously presenting the view controller, which might not even be necessary. Someone with more SwiftUI experience might be able to offer suggestions for improvement.
import Foundation
import SwiftUI
import UIKit
struct ActivityViewController: UIViewControllerRepresentable {
#Binding var shareURL: URL?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> some UIViewController {
let containerViewController = UIViewController()
return containerViewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
guard let shareURL = shareURL, context.coordinator.presented == false else { return }
context.coordinator.presented = true
let activityViewController = UIActivityViewController(activityItems: [shareURL], applicationActivities: nil)
activityViewController.completionWithItemsHandler = { activity, completed, returnedItems, activityError in
self.shareURL = nil
context.coordinator.presented = false
if completed {
// ...
} else {
// ...
}
}
// Executing this asynchronously might not be necessary but some of my tests
// failed because the view wasn't yet in the view hierarchy on the first pass of updateUIViewController
//
// There might be a better way to test for that condition in the guard statement and execute this
// synchronously if we can be be sure updateUIViewController is invoked at least once after the view is added
DispatchQueue.main.asyncAfter(deadline: .now()) {
uiViewController.present(activityViewController, animated: true)
}
}
class Coordinator: NSObject {
let parent: ActivityViewController
var presented: Bool = false
init(_ parent: ActivityViewController) {
self.parent = parent
}
}
}
struct ContentView: View {
#State var shareURL: URL? = nil
var body: some View {
ZStack {
Button(action: { shareURL = URL(string: "https://apple.com") }) {
Text("Share")
.foregroundColor(.white)
.padding()
}
.background(Color.blue)
if shareURL != nil {
ActivityViewController(shareURL: $shareURL)
}
}
.frame(width: 375, height: 812)
}
}
iOS 15 / Swift 5 / Xcode 13
Extension to get the top presented UIViewController:
import UIKit
extension UIApplication {
// MARK: No shame!
static func TopPresentedViewController() -> UIViewController? {
guard let rootViewController = UIApplication.shared
.connectedScenes.lazy
.compactMap({ $0.activationState == .foregroundActive ? ($0 as? UIWindowScene) : nil })
.first(where: { $0.keyWindow != nil })?
.keyWindow?
.rootViewController
else {
return nil
}
var topController = rootViewController
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
}
Then use it to present your UIActivityViewController:
UIApplication.TopPresentedViewController?.present(activityViewController, animated: true, completion: nil)
Original Answer (deprecated code):
It's not pretty but you can call it directly like this (considering your app has only 1 window):
UIApplication.shared.windows.first?.rootViewController?.present(activityViewController, animated: true, completion: nil)
And if you get some warning blablabla:
Warning: Attempt to present ... which is already presenting ...
you can do something like this to get the top most view controller and call present on it.
There's a UIModalPresentationStyle which can be used to display certain presentations:
case pageSheet
A presentation style that partially covers the underlying content.
The way you apply the presentation style:
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
let v = UIActivityViewController(activityItems: sharing, applicationActivities: nil)
v.modalPresentationStyle = .pageSheet
return v
}
A list of the Presentations can be found here:
https://developer.apple.com/documentation/uikit/uimodalpresentationstyle
I haven't yet tested them all myself so I apologise in advance if this didn't end up working like you expected it to.
Alternatively you can have a look at this answer where they mention a third-party library, which will allow you to create a half modal in the way that it's usually presented.

SwiftUI, tvOS, UIViewControllerRepresentable and a focus issue

I'm trying to implement search functionality in the tvOS SwiftUI app. I'm using UISearchController as the most straight forward solution to do this. I've wrapped it inside SearchView which conforms to UIViewControllerRepresentable. The problem is, that it looks like the focus engine refuses to focus on a part of a view controller UI - UISearchBar that is wrapped. I can type the search query from my Mac, inside the simulator, to verify that the search works, but of course, it's not the real thing.
I've tried to add the .focusable() modifier into a SearchView, but it didn't help.
Also tried to implement shouldUpdateFocus, preferredFocusEnvironments and didUpdateFocus callbacks inside my custom subclasses of UISearchController and UISearchContainerViewController but those are not called at all.
I think I'm missing something very straightforward here.
Here is the code for the SearchView:
struct SearchView: UIViewControllerRepresentable {
#Binding var text: String
typealias UIViewControllerType = UISearchContainerViewController
typealias Context = UIViewControllerRepresentableContext<SearchView>
func makeUIViewController(context: Context) -> UIViewControllerType {
let controller = UISearchController(searchResultsController: context.coordinator)
controller.searchResultsUpdater = context.coordinator
return UISearchContainerViewController(searchController: controller)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
func makeCoordinator() -> SearchView.Coordinator {
return Coordinator(text: $text)
}
class Coordinator: UIViewController, UISearchResultsUpdating {
#Binding var text: String
init(text: Binding<String>) {
_text = text
super.init(nibName: nil, bundle: nil)
}
func updateSearchResults(for searchController: UISearchController) {
guard let searchText = searchController.searchBar.text else { return }
text = searchText
}
}
}
And the main ContentView (I've stripped some non-important code):
struct ContentView: View {
#State var model = ["aa", "ab", "bb", "bc", "cc", "dd", "ee"]
#State var searchQuery: String = ""
var body: some View {
SearchView(text: $searchQuery)
List {
ForEach(model.filter({ $0.hasPrefix(searchQuery) })) { item in
Text(item)
}
}
}
}
Finally, it can be done through the .searchable modifier, introduced in iOS 15 beta 1, no need for UISearchController wrappers anymore.
P.S. Got an official response from Apple on this bug (FB8974300) to also use .searchable modifier, so there is no fix for the older versions.

how to create keyboard app extension in swiftui?

I am new to swiftUI.I need to create a keyboard extension in swiftui. I just can't find out how to do that. I am searching on internet for whole day but still can't find out how to do that.
Here is some code that I wrote :
struct ContentView: View {
var body: some View {
VStack{
keyboard()
}
}
}
struct keyboard: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIInputViewController {
let inputVC = UIInputViewController()
return inputVC
}
func updateUIViewController(_ uiViewController: UIInputViewController, context: Context) {
print("some text")
}
}
The above code is written in extension folder's keyboardViewController.swift file and not giving me any kind of keyboard display.
IF I write UIKit UIInputController (the file created itself when we create an extension) code in same file then only I can see a keyboard extension appearing.
I want to design keyboard in UIKit Inputviewcontroller type of class and then display it using UIViewControllerRepresentable in swiftui contentview.
Now my question is-> Is this Approach right?? IF yes then please guide me ahead. IF no then please suggest me the right approach.
Thanks in advance!!
Here's my approach using UIHostingController:
When I added a new target "Keyboard" of keyboard extension, XCode automatically generated a class KeyboardViewController:
class KeyboardViewController: UIInputViewController {
// some code
override func viewDidLoad() {
super.viewDidLoad()
self.nextKeyboardButton = UIButton(type: .system)
// some code
}
// some code
}
I added the view through UIHostingController with my keyboard view as the rootView of it. Then added it and its view as children of KeyboardViewController and the view of KeyboardViewController:
class KeyboardViewController: UIInputViewController {
// some code
override func viewDidLoad() {
super.viewDidLoad()
let hostingController = UIHostingController(rootView: KeyboardView(viewController: self))
view.addSubview(hostingController.view)
addChild(hostingController)
self.nextKeyboardButton = UIButton(type: .system)
// some code
}
// some code
}
And my KeyboardView was like:
struct KeyboardView: View {
var viewController: KeyboardViewController
var body: some View {
// some view as you designed
}
}
It worked for me.