How to set up Firebase Google SignIn 6.0.2 using SwiftUI 2.0 architecture? - swift

I'm trying to set up the latest GoogleSignIn with SwiftUI. Official Firebase documentation offers outdated approach designed for UIKit, ViewControllers and AppDelegate, so there is simply no way to find out what workaround is needed to make it work. I was able to implement UIApplicationDelegateAdaptor to get access to didFinishLaunchingWithOptions method to configure FirebaseApp and GIDSignIn.
While following this documentation: https://firebase.google.com/docs/auth/ios/google-signin I ultimately stuck on the step 4. It's unclear where do I have to use this code or how to integrate it into SwiftUI paradigm:
guard let clientID = FirebaseApp.app()?.options.clientID else { return }
// Create Google Sign In configuration object.
let config = GIDConfiguration(clientID: clientID)
// Start the sign in flow!
GIDSignIn.sharedInstance.signIn(with: config, presenting: self) { [unowned self] user, error in
if let error = error {
// ...
return
}
guard
let authentication = user?.authentication,
let idToken = authentication.idToken
else {
return
}
let credential = GoogleAuthProvider.credential(withIDToken: idToken,
accessToken: authentication.accessToken)
// ...
}
I'm aware of this guide from Google: https://developers.google.com/identity/sign-in/ios/quick-migration-guide but there is no code example of how to do it properly.

You would probably want to run that code as the result of an action the user takes. After pressing a Button, for example.
The tricky part is going to be that GIDSignIn.sharedInstance.signIn wants a UIViewController for its presenting argument, which isn't necessarily straightforward to get in SwiftUI. You can use a UIViewControllerRepresentable to get a reference to a view controller in the hierarchy that you can present from.
class LoginManager : ObservableObject {
var viewController : UIViewController?
func runLogin() {
guard let viewController = viewController else {
fatalError("No view controller")
}
//Other GIDSignIn code here. Use viewController for the `presenting` argument
}
}
struct DummyViewController : UIViewControllerRepresentable {
var loginManager : LoginManager
func makeUIViewController(context: Context) -> some UIViewController {
let vc = UIViewController()
loginManager.viewController = vc
return vc
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
struct ContentView : View {
#StateObject private var loginManager = LoginManager()
var body: some View {
VStack(spacing: 0) {
Button(action: {
loginManager.runLogin()
}) {
Text("Login")
DummyViewController(loginManager: loginManager)
.frame(width: 0, height: 0)
}
}
}
}

Related

How to authenticate with the Box SDK using SwiftUI?

I have been having a hard time trying to figure out how to authenticate with the Box API using SwiftUI.
As far as I understand, SwiftUI does not currently have the ability to satisfy the ASWebAuthenticationPresentationContextProviding protocol required to show the Safari OAuth2 login sheet. I know that I can make a UIViewControllerRepresentable to use UIKit within SwiftUI, but I can't get this to work.
I have figured out how to get the OAuth2 login sheet for Dropbox to appear and authenticate the client using SwiftUI.
The trick is to use a Coordinator to make the UIViewControllerRepresentable satisfy a protocol.
import SwiftUI
import BoxSDK
import AuthenticationServices
var boxSDK = BoxSDK(clientId: "<Client ID>", clientSecret: "<Client Secret>")
var boxClient: BoxClient
struct BoxLoginView: View {
#State var showLogin = false
var body: some View {
VStack {
Button {
showLogin = true
} label: {
Text("Login")
}
BoxView(isShown: $showLogin)
// Arbitrary frame size so that this view does not take up the whole screen
.frame(width: 40, height: 40)
}
}
}
/// A UIViewController that will present the OAuth2 Safari login screen when the isShown is true.
struct BoxView: UIViewControllerRepresentable {
typealias UIViewControllerType = UIViewController
let letsView = UIViewController()
#Binding var isShown : Bool
// Show the login Safari window when isShown
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
if(isShown) {
getOAuthClient()
}
}
func makeUIViewController(context _: Self.Context) -> UIViewController {
return self.letsView
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
func getOAuthClient() {
boxSDK.getOAuth2Client(tokenStore: KeychainTokenStore(), context:self.makeCoordinator()) { result in
switch result {
case let .success(client):
boxClient = client
case let .failure(error):
print("error in getOAuth2Client: \(error)")
}
}
}
class Coordinator: NSObject, ASWebAuthenticationPresentationContextProviding {
var parent: BoxView
init(parent: BoxView) {
self.parent = parent
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return parent.letsView.view.window ?? ASPresentationAnchor()
}
}
}

SwiftUI: Display file url from Array after user Picks file using DocumentPicker

I followed a tutorial on getting Url from a Document a user chooses and be able to display it on the View. My problem now is I want to add those Url's into an array. Then get the items from the array and print them onto the View. The way it works is the User presses a button and a sheet pops up with the files app. There the user is able to choose a document. After the user chooses the document the Url is printed on the View. To print the Url is use this
//if documentUrl has an Url show it on the view
If let url= documentUrl{
Text(url.absoluteString)
}
Issue with this is that when I do the same thing the
If let url= documentUrl
Is ran before the Url is even added to the array and the app crashes
Here is the full code
//Add the Urls to the array
class Article: ObservableObject{
var myArray:[String] = []
}
struct ContentView: View {
#State private var showDocumentPicker = false
#State private var documentUrl:URL?
#State var myString:URL?
#ObservedObject var userData:Article
// Func for onDismiss from the Sheet
func upload() {
// add the Url to the Array
DispatchQueue.main.async{
userData.myArray.append(documentUrl!.absoluteString)
}
}
var body: some View {
VStack{
//If have Url reflect that on the View
if let url = documentUrl{
//Works
Text(url.absoluteString)
//doesntwork
Text(userData.myArray[0])
}
}
Button(action:{showDocumentPicker.toggle()},
label: {
Text("Select your file")
})
.sheet(isPresented: $showDocumentPicker, onDismiss: upload )
{
DocumentPicker(url: $documentUrl)
}
}
}
The main thing I want to do the just display the ulrs into the view after the user chooses the document or after the sheet disappears. So if the user chooses 1 Url only one is printed. If another one is chosen after then 2 are show etc.
This is the documentPicker code used to choose a document
struct DocumentPicker : UIViewControllerRepresentable{
#Binding var url : URL?
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
//initialize a UI Document Picker
let viewController = UIDocumentPickerViewController(forOpeningContentTypes: [.epub])
viewController.delegate = context.coordinator
print("1")
return viewController
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {
print("Swift just updated ")
print("2")
}
}
extension DocumentPicker{
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator:NSObject, UIDocumentPickerDelegate{
let parent: DocumentPicker
init(_ documentPicker: DocumentPicker){
self.parent = documentPicker
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else{return}
parent.url = url
print("3")
}
}
}
Not sure if maybe I'm not approaching this the correct way? I looked at different tutorial but couldn't find anything.
Use .fileImporter presentation modifire (above ios 14)
.fileImporter(isPresented: $showDocumentPicker,
allowedContentTypes: [.image],
allowsMultipleSelection: true)
{ result in
// processing results Result<[URL], Error>
}
An observable object doesn't have a change trigger. To inform that the observable object has changed use one of the following examples:
class Article: ObservableObject {
#Published var myArray:[String] = []
}
or
class Article: ObservableObject {
private(set) var myArray:[String] = [] {
willSet {
objectWillChange.send()
}
}
func addUrl(url: String) {
myArray.append(url)
}
}
official documentation: https://developer.apple.com/documentation/combine/observableobject

SwiftUI Facebook Login - Mobile App STILL crashes after injecting Environment Object

I keep receiving the error: Thread 1: Fatal error: No ObservableObject of type User found. A View.environmentObject(_:) for User may be missing as an ancestor of this view.
This is my code below for Facebook login via Swift. The specific code that causes it to crash is self.user.isActive = true. This code accesses my environment object User and changes the variable isActive to True, which triggers my Navigation Link and causes the DetailView to appear.
Given the error, I have tried multiple ways to inject the Environment variable:
I added it like this when I used login in the view: login().environmentObject(User()).frame(width: 50, height: 50)
I tried to initialize the Coordinator class with login as a parent, and then access the user.isActive variable via the parent relationship (i.e. self.parent.user.isActive = true) but this does not change the environment variable or push the screen.
I tried to use a navigation link directly in the Coordinator class but I cannot use the self.user.isActive as a binding bool.
struct login : UIViewRepresentable {
#EnvironmentObject var user: User
let userDefault = UserDefaults.standard
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<login>) -> FBLoginButton {
let button = FBLoginButton()
button.permissions = ["public_profile", "email"]
button.delegate = context.coordinator
return button
}
func updateUIView(_ uiView: FBLoginButton, context: UIViewRepresentableContext<login>) {
// if self.userDefault.bool(forKey: Constants.UserDefaults.currentUser) {self.user.isActive = true}
}
func activateLogin(){
if self.userDefault.bool(forKey: Constants.UserDefaults.currentUser)
{ self.user.isActive = true }
}
class Coordinator : NSObject, LoginButtonDelegate {
#EnvironmentObject var user: User
let userDefault = UserDefaults.standard
var parent: login
init(_ parent: login) {
self.parent = parent
}
func loginButton(_ loginButton: FBLoginButton, didCompleteWith result: LoginManagerLoginResult?, error: Error?) {
if error != nil{
print((error?.localizedDescription)!)
return
}
if AccessToken.current != nil{
let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)
Auth.auth().signIn(with: credential) { (res,er) in
if er != nil{
print((er?.localizedDescription)!)
return
}
print("success")
self.user.isActive = true
self.parent.user.isActive = true
self.userDefault.set(true, forKey: Constants.UserDefaults.currentUser)
self.userDefault.synchronize()
}
}
}
func loginButtonDidLogOut(_ loginButton: FBLoginButton) {
try! Auth.auth().signOut()
print ("User logged out")
}
}
}```
My guess is that you don't have nice #EnvironmentObject set up for you in traditional UIKit views.
Maybe you can do it in views that conform to UIViewRepresentable, since it is meant to work with UIKit.
But I don't think a class Coordinator: NSObject is able to support #EnvironmentObject.

Using ASWebAuthentication in SwiftUI

Having a bit of trouble getting authentication to work from within a SwiftUI view. I’m using ASWebAuthentication and whenever I run I get an error:
Cannot start ASWebAuthenticationSession without providing presentation context. Set presentationContextProvider before calling -start.
I’m creating a ViewController and passing in a reference to the Scene Delegate window based on this stack overflow post but that answer doesn’t seem to be working for me. I’ve also found this reddit post, but I’m a little unclear as to how they were able to initialize the view with the window before the scene delegate’s window is set up.
This is the code I’m using for the SwiftUI view:
import SwiftUI
import AuthenticationServices
struct Spotify: View {
var body: some View {
Button(action: {
self.authWithSpotify()
}) {
Text("Authorize Spotify")
}
}
func authWithSpotify() {
let authUrlString = "https://accounts.spotify.com/authorize?client_id=\(spotifyID)&response_type=code&redirect_uri=http://redirectexample.com/callback&scope=user-read-private%20user-read-email"
guard let url = URL(string: authUrlString) else { return }
let session = ASWebAuthenticationSession(
url: url,
callbackURLScheme: "http://redirectexample.com/callback",
completionHandler: { callback, error in
guard error == nil, let success = callback else { return }
let code = NSURLComponents(string: (success.absoluteString))?.queryItems?.filter({ $0.name == "code" }).first
self.getSpotifyAuthToken(code)
})
session.presentationContextProvider = ShimViewController()
session.start()
}
func getSpotifyAuthToken(_ code: URLQueryItem?) {
// Get Token
}
}
struct Spotify_Previews: PreviewProvider {
static var previews: some View {
Spotify()
}
}
class ShimViewController: UIViewController, ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return globalPresentationAnchor ?? ASPresentationAnchor()
}
}
And in the SceneDelegate:
var globalPresentationAnchor: ASPresentationAnchor? = nil
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Use a UIHostingController as window root view controller
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: Spotify())
self.window = window
window.makeKeyAndVisible()
}
globalPresentationAnchor = window
}
Any idea how I can make this work?
Regarding the reddit post, I got it to work as is. My misunderstanding was that the AuthView isn't being used as an 'interface' View. I created a normal SwiftUI View for my authentication view, and I have a Button with the action creating an instance of the AuthView, and calling the function that handles the session. I'm storing the globalPositionAnchor in an #EnvironmentObject, but you should be able to use it from the global variable as well. Hope this helps!
struct SignedOutView: View {
#EnvironmentObject var contentManager: ContentManager
var body: some View {
VStack {
Text("Title")
.font(.largeTitle)
Spacer()
Button(action: {AuthProviderView(window: self.contentManager.globalPresentationAnchor!).signIn()}) {
Text("Sign In")
.padding()
.foregroundColor(.white)
.background(Color.orange)
.cornerRadius(CGFloat(5))
.font(.headline)
}.padding()
}
}
}
I've run into something similar before when implementing ASWebAuthenticationSession. One thing I didn't realize, is you have to have a strong reference to the session variable. So I would make you session variable a property of your class and see if that fixes the issue. A short snippet of what I mean:
// initialize as a property of the class
var session: ASWebAuthenticationSession?
func authWithSpotify() {
let authUrlString = "https://accounts.spotify.com/authorize?client_id=\(spotifyID)&response_type=code&redirect_uri=http://redirectexample.com/callback&scope=user-read-private%20user-read-email"
guard let url = URL(string: authUrlString) else { return }
// assign session here
session = ASWebAuthenticationSession(url: url, callbackURLScheme: "http://redirectexample.com/callback", completionHandler: { callback, error in
guard error == nil, let success = callback else { return }
let code = NSURLComponents(string: (success.absoluteString))?.queryItems?.filter({ $0.name == "code" }).first
self.getSpotifyAuthToken(code)
})
session.presentationContextProvider = ShimViewController()
session.start()
}
Ronni -
I ran into the same problem but finally got the ShimController() to work and avoid the warning. I got sucked into the solution but forgot to instantiate the class. Look for my "<<" comments below. Now the auth is working and the callback is firing like clockwork. The only caveat here is I'm authorizing something else - not Spotify.
var session: ASWebAuthenticationSession?
var shimController = ShimViewController() // << instantiate your object here
func authWithSpotify() {
let authUrlString = "https://accounts.spotify.com/authorize?client_id=\(spotifyID)&response_type=code&redirect_uri=http://redirectexample.com/callback&scope=user-read-private%20user-read-email"
guard let url = URL(string: authUrlString) else { return }
// assign session here
session = ASWebAuthenticationSession(url: url, callbackURLScheme: "http://redirectexample.com/callback", completionHandler: { callback, error in
guard error == nil, let success = callback else { return }
let code = NSURLComponents(string: (success.absoluteString))?.queryItems?.filter({ $0.name == "code" }).first
self.getSpotifyAuthToken(code)
})
session.presentationContextProvider = shimController // << then reference it here
session.start()
}
Using .webAuthenticationSession(isPresented:content) modifier in BetterSafariView, you can easily start a web authentication session in SwiftUI. It doesn't need to hook SceneDelegate.
import SwiftUI
import BetterSafariView
struct SpotifyLoginView: View {
#State private var showingSession = false
var body: some View {
Button("Authorize Spotify") {
self.showingSession = true
}
.webAuthenticationSession(isPresented: $showingSession) {
WebAuthenticationSession(
url: URL(string: "https://accounts.spotify.com/authorize")!,
callbackURLScheme: "myapp"
) { callbackURL, error in
// Handle callbackURL
}
}
}
}

SwiftUI exporting or sharing files

I'm wondering if there is a good way export or share a file through SwiftUI. There doesn't seem to be a way to wrap a UIActivityViewController and present it directly. I've used the UIViewControllerRepresentable to wrap a UIActivityViewController, and it crashes if I, say, present it in a SwiftUI Modal.
I was able to create a generic UIViewController and then from there call a method that presents the UIActivityViewController, but that's a lot of wrapping.
And if we want to share from the Mac using SwiftUI, is there a way to wrap NSSharingServicePicker?
Anyway, if anyone has an example of how they're doing this, it would be much appreciated.
You can define this function anywhere (preferably in the global scope):
#discardableResult
func share(
items: [Any],
excludedActivityTypes: [UIActivity.ActivityType]? = nil
) -> Bool {
guard let source = UIApplication.shared.windows.last?.rootViewController else {
return false
}
let vc = UIActivityViewController(
activityItems: items,
applicationActivities: nil
)
vc.excludedActivityTypes = excludedActivityTypes
vc.popoverPresentationController?.sourceView = source.view
source.present(vc, animated: true)
return true
}
You can use this function in a button action, or anywhere else needed:
Button(action: {
share(items: ["This is some text"])
}) {
Text("Share")
}
We can call the UIActivityViewController directly from the View (SwiftUI) without using UIViewControllerRepresentable.
import SwiftUI
enum Coordinator {
static func topViewController(_ viewController: UIViewController? = nil) -> UIViewController? {
let vc = viewController ?? UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController
if let navigationController = vc as? UINavigationController {
return topViewController(navigationController.topViewController)
} else if let tabBarController = vc as? UITabBarController {
return tabBarController.presentedViewController != nil ? topViewController(tabBarController.presentedViewController) : topViewController(tabBarController.selectedViewController)
} else if let presentedViewController = vc?.presentedViewController {
return topViewController(presentedViewController)
}
return vc
}
}
struct ActivityView: View {
var body: some View {
Button(action: {
self.shareApp()
}) {
Text("Share")
}
}
}
extension ActivityView {
func shareApp() {
let textToShare = "something..."
let activityViewController = UIActivityViewController(activityItems: [textToShare], applicationActivities: nil)
let viewController = Coordinator.topViewController()
activityViewController.popoverPresentationController?.sourceView = viewController?.view
viewController?.present(activityViewController, animated: true, completion: nil)
}
}
struct ActivityView_Previews: PreviewProvider {
static var previews: some View {
ActivityView()
}
}
And this is a preview:
Hoping to help someone!
EDIT: Removed all code and references to UIButton.
Thanks to #Matteo_Pacini for his answer to this question for showing us this technique. As with his answer (and comment), (1) this is rough around the edges and (2) I'm not sure this is how Apple wants us to use UIViewControllerRepresentable and I really hope they provide a better SwiftUI ("SwiftierUI"?) replacement in a future beta.
I put in a lot of work in UIKit because I want this to look good on an iPad, where a sourceView is needed for the popover. The real trick is to display a (SwiftUI) View that gets the UIActivityViewController in the view hierarchy and trigger present from UIKit.
My needs were to present a single image to share, so things are targeted in that direction. Let's say you have an image, stored as a #State variable - in my example the image is called vermont.jpg and yes, things are hard-coded for that.
First, create a UIKit class of type `UIViewController to present the share popover:
class ActivityViewController : UIViewController {
var uiImage:UIImage!
#objc func shareImage() {
let vc = UIActivityViewController(activityItems: [uiImage!], applicationActivities: [])
vc.excludedActivityTypes = [
UIActivity.ActivityType.postToWeibo,
UIActivity.ActivityType.assignToContact,
UIActivity.ActivityType.addToReadingList,
UIActivity.ActivityType.postToVimeo,
UIActivity.ActivityType.postToTencentWeibo
]
present(vc,
animated: true,
completion: nil)
vc.popoverPresentationController?.sourceView = self.view
}
}
The main things are;
You need a "wrapper" UIViewController to be able to present things.
You need var uiImage:UIImage! to set the activityItems.
Next up, wrap this into a UIViewControllerRepresentable:
struct SwiftUIActivityViewController : UIViewControllerRepresentable {
let activityViewController = ActivityViewController()
func makeUIViewController(context: Context) -> ActivityViewController {
activityViewController
}
func updateUIViewController(_ uiViewController: ActivityViewController, context: Context) {
//
}
func shareImage(uiImage: UIImage) {
activityViewController.uiImage = uiImage
activityViewController.shareImage()
}
}
The only two things of note are:
Instantiating ActivityViewController to return it up to ContentView
Creating shareImage(uiImage:UIImage) to call it.
Finally, you have ContentView:
struct ContentView : View {
let activityViewController = SwiftUIActivityViewController()
#State var uiImage = UIImage(named: "vermont.jpg")
var body: some View {
VStack {
Button(action: {
self.activityViewController.shareImage(uiImage: self.uiImage!)
}) {
ZStack {
Image(systemName:"square.and.arrow.up").renderingMode(.original).font(Font.title.weight(.regular))
activityViewController
}
}.frame(width: 60, height: 60).border(Color.black, width: 2, cornerRadius: 2)
Divider()
Image(uiImage: uiImage!)
}
}
}
Note that there's some hard-coding and (ugh) force-unwrapping of uiImage, along with an unnecessary use of #State. These are there because I plan to use `UIImagePickerController next to tie this all together.
The things of note here:
Instantiating SwiftUIActivityViewController, and using shareImage as the Button action.
Using it to also be button display. Don't forget, even a UIViewControllerRepresentable is really just considered a SwiftUI View!
Change the name of the image to one you have in your project, and this should work. You'll get a centered 60x60 button with the image below it.
Most of the solutions here forget to populate the share sheet on the iPad.
So, if you intend to have an application not crashing on this device, you can use
this method where popoverController is used and add your desired activityItems as a parameter.
import SwiftUI
/// Share button to populate on any SwiftUI view.
///
struct ShareButton: View {
/// Your items you want to share to the world.
///
let itemsToShare = ["https://itunes.apple.com/app/id1234"]
var body: some View {
Button(action: { showShareSheet(with: itemsToShare) }) {
Image(systemName: "square.and.arrow.up")
.font(.title2)
.foregroundColor(.blue)
}
}
}
extension View {
/// Show the classic Apple share sheet on iPhone and iPad.
///
func showShareSheet(with activityItems: [Any]) {
guard let source = UIApplication.shared.windows.last?.rootViewController else {
return
}
let activityVC = UIActivityViewController(
activityItems: activityItems,
applicationActivities: nil)
if let popoverController = activityVC.popoverPresentationController {
popoverController.sourceView = source.view
popoverController.sourceRect = CGRect(x: source.view.bounds.midX,
y: source.view.bounds.midY,
width: .zero, height: .zero)
popoverController.permittedArrowDirections = []
}
source.present(activityVC, animated: true)
}
}
Take a look at AlanQuatermain -s SwiftUIShareSheetDemo
In a nutshell it looks like this:
#State private var showShareSheet = false
#State public var sharedItems : [Any] = []
Button(action: {
self.sharedItems = [UIImage(systemName: "house")!]
self.showShareSheet = true
}) {
Text("Share")
}.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: self.sharedItems)
}
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
let activityItems: [Any]
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
let callback: Callback? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
// nothing to do here
}
}