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

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

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

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

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

Download Image for Firebase Storage inside a SwiftUI Listview (contact app)

I'm new in Swift and I'm trying to display inside a SwiftUI ListView :
some datas
an image (linked to datas)
It's kind of a contact app.
All these datas are stored in firestore. I created a function which gives the image URL on firestore :
func getURL(path: String, completion: #escaping (((URL?) -> Void))) {
let storage = Storage.storage()
storage.reference().child(path).downloadURL(completion: { url, error in
guard let url = url, error == nil else {
return
}
let urlPath = url.absoluteURL
completion(urlPath)
})
}
But when i call this function in the SwiftUI View, the following error appears :
"Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols"
There is the calling code of the function :
getURL(path: bike.access1, completion: { path in AnimatedImage(url: path)})
I'm aware that i can't call function inside a view, but i don't see how to manage to display an image from Firestore inside a SwiftUI List View.
If anyone know a strategy, I'm really interested.
Thanks in advance
Jean
The completion handler is (in general) a great way of dealing with asynchronous code. But, it a View in SwiftUI, it's a little more common to use a #State or #Published value and then render the view conditionally based on its state.
I like using an ObservableObject view model for this sort of thing:
class ViewModel : ObservableObject {
#Published var imageURL : URL?
func getURL(path: String) {
let storage = Storage.storage()
storage.reference().child(path).downloadURL(completion: { url, error in
guard let url = url, error == nil else {
return
}
self.imageURL = url
})
}
}
struct ContentView : View {
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
if let url = viewModel.imageURL {
AnimatedImage(url: url)
}
}.onAppear {
viewModel.getURL(path: "URL_STRING_GOES_HERE")
}
}
}
Note that my types

SwiftUI using NSSharingServicePicker in MacOS

I am trying to use a Share function inside my MacOS app in SwiftUI. I am having a URL to a file, which I want to share. It can be images/ documents and much more.
I found NSSharingServicePicker for MacOS and would like to use it. However, I am struggeling to use it in SwiftUI.
Following the documentation, I am creating it like this:
let shareItems = [...]
let sharingPicker : NSSharingServicePicker = NSSharingServicePicker.init(items: shareItems as [Any])
sharingPicker.show(relativeTo: NSZeroRect, of:shareView, preferredEdge: .minY)
My problem is in that show() method. I need to set a NSRect, where I can use NSZeroRect.. but I am struggeling with of: parameter. It requires a NSView. How can I convert my current view as NSView and use it that way. Or can I use my Button as NSView(). I am struggling with that approach.
Another option would be to use a NSViewRepresentable. But should I just create a NSView and use it for that method.
Here is minimal working demo example
struct SharingsPicker: NSViewRepresentable {
#Binding var isPresented: Bool
var sharingItems: [Any] = []
func makeNSView(context: Context) -> NSView {
let view = NSView()
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
if isPresented {
let picker = NSSharingServicePicker(items: sharingItems)
picker.delegate = context.coordinator
// !! MUST BE CALLED IN ASYNC, otherwise blocks update
DispatchQueue.main.async {
picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(owner: self)
}
class Coordinator: NSObject, NSSharingServicePickerDelegate {
let owner: SharingsPicker
init(owner: SharingsPicker) {
self.owner = owner
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) {
// do here whatever more needed here with selected service
sharingServicePicker.delegate = nil // << cleanup
self.owner.isPresented = false // << dismiss
}
}
}
Demo of usage:
struct TestSharingService: View {
#State private var showPicker = false
var body: some View {
Button("Share") {
self.showPicker = true
}
.background(SharingsPicker(isPresented: $showPicker, sharingItems: ["Message"]))
}
}
Another option without using NSViewRepresentable is:
extension NSSharingService {
static func submenu(text: String) -> some View {
return Menu(
content: {
ForEach(items, id: \.title) { item in
Button(action: { item.perform(withItems: [text]) }) {
Image(nsImage: item.image)
Text(item.title)
}
}
},
label: {
Image(systemName: "square.and.arrow.up")
}
)
}
}
You lose things like the "more" menu item or recent recipients. But in my opinion it's more than enough, simple and pure SwiftUI.

LinkPresentation views not fully loading in SwiftUI

I'm using code I found from a great article here that demonstrates how to use the LinkPresentation framework in SwiftUI.
However I'm having a small problem that I can't find solution to - the link previews loads their metadata but don't refresh the view once fully loaded unless I do something which forces the view to refresh, like rotating the phone.
They load as much as this:
Then look like this after rotating:
I'd like the views to fully refresh once the metadata is loaded. I feel like I probably need to add some binding in somewhere but I don't know where. Can anyone help at all?
Here's the UIViewRepresentable
import SwiftUI
import LinkPresentation
struct URLPreview : UIViewRepresentable {
var previewURL:URL
func makeUIView(context: Context) -> LPLinkView {
LPLinkView(url: previewURL)
}
func updateUIView(_ view: LPLinkView, context: Context) {
// New instance for each update
let provider = LPMetadataProvider()
provider.startFetchingMetadata(for: previewURL) { (metadata, error) in
if let md = metadata {
DispatchQueue.main.async {
view.metadata = md
view.sizeToFit()
}
}
}
}
}
and here's how it's called:
struct Content: View {
var body: some View {
URLPreview(previewURL: URL(string: "www.apple.com")!)
}
}
Triggering a redraw is what you need. Not a fan of this, but you can try Binding a State CGSize and set frame to width/height.
struct URLPreview : UIViewRepresentable {
var previewURL:URL
//Add binding
#Binding var metaSize: CGSize
func makeUIView(context: Context) -> LPLinkView {
LPLinkView(url: previewURL)
}
func updateUIView(_ view: LPLinkView, context: Context) {
// New instance for each update
let provider = LPMetadataProvider()
provider.startFetchingMetadata(for: previewURL) { (metadata, error) in
if let md = metadata {
DispatchQueue.main.async {
view.metadata = md
view.sizeToFit()
//Set binding after resize
self.metaSize = view.frame.size
}
}
}
}
}
struct ContentView: View {
//can default original state
#State var metaSize: CGSize = CGSize()
var body: some View {
URLPreview(previewURL: URL(string: "www.apple.com")!, metaSize: $metaSize)
.frame(width: metaSize.width, height: metaSize.height)
}
}
UPDATE
NSPratik is right, the solution is not really viable for Lists. So an amended solution is actually just to use a simple Bool State to toggle the Views generated by a list:
struct ContentView: View {
//can default original state
#State var togglePreview = false
let urls: [String] = ["https://medium.com","https://apple.com","https://yahoo.com","https://stackoverflow.com"]
var body: some View {
List(urls, id: \.self) { url in
URLPreview(previewURL: URL(string: url)!, togglePreview: self.$togglePreview)
.aspectRatio(contentMode: .fit)
.padding()
}
}
}
struct URLPreview : UIViewRepresentable {
var previewURL:URL
//Add binding
#Binding var togglePreview: Bool
func makeUIView(context: Context) -> LPLinkView {
let view = LPLinkView(url: previewURL)
let provider = LPMetadataProvider()
provider.startFetchingMetadata(for: previewURL) { (metadata, error) in
if let md = metadata {
DispatchQueue.main.async {
view.metadata = md
view.sizeToFit()
self.togglePreview.toggle()
}
}
}
return view
}
func updateUIView(_ uiView: LPLinkView, context: UIViewRepresentableContext<URLPreview>) {
}
}
We simply use togglePreview as our trigger, pass it to a Binding var in the UIView, and then setup our List. Even if this triggers all the Views in the List, there won't be any animation to reflect the resize of fully loaded LinkViews.
Using LPLinkViews in a List causes huge memory leaks. Your best bet is to use a VStack embedded inside a ScrollView.
ScrollView {
VStack {
ForEach(links, id: \.self) { link in
if let url = URL(string: link) {
LinkRow(url: url)
}
}
}
.padding()
}
This will make the LPLinkViews resize themselves as they load.
I have done this in an app and it has significant improvement over using a List. However a little caveat, if the user stars scrolling up and down as soon as the view comes on screen while the previews are still loading, it might causes crashes at random. Unfortunately I haven't been able to find a solution for that yet. I think all these crashes happen because the LPMetadataProvider requires you to be called on the main thread and obviously that doesn't play well with smooth scrolling.