Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: How to save URL-Bookmarks with security scope

.fileImporter( isPresented: $openFile, allowedContentTypes: [.image,.pdf], allowsMultipleSelection: true, onCompletion: { result in
        
        switch result {
        case .success(let files):
            fileURL = []
            files.forEach { file in
                let gotAccess = file.startAccessingSecurityScopedResource()
                if !gotAccess { return }
                if let imageData = try? Data(contentsOf: file),
                   let image = UIImage(data: imageData) {
                    images.append(image)
                }
                fileURL = files
                file.stopAccessingSecurityScopedResource()
            }
        case .failure(let error):
            print(error)
        }
    })

The fileURL could be e.g. @ApplicationStorage("url's"). How can I use the fileURL later? I think that I have to store them as bookmarks with SecuritySscope, but I don't know how to do.

Any ideas? Thanks in advance for helping hints. Peter

like image 328
Peter Plass Avatar asked Nov 01 '25 23:11

Peter Plass


1 Answers

When you use fileImporter(isPresented:allowedContentTypes:allowsMultipleSelection:onCompletion:), SwiftUI returns security-scoped URLs where you need to state when you're accessing a resource (you can only have so many open before the system starts rejecting your requests).

If you want persisted access to a URL (where you can access it even when your app exits), you'll need to create a bookmark for the URL via bookmarkData(options:includingResourceValuesForKeys:relativeTo:), passing at least .withSecurityScope to the options parameter. Note that, depending on the kind of access you need, you may need to pass more information. For example, if you only need read access to a URL, also pass .securityScopeAllowOnlyReadAccess to the options. And if you plan on working with an arbitrary amount of URLs (say, up to the thousands), you should also pass the .withoutImplicitSecurityScope option.

Whenever you want to retrieve the URL for the bookmark, create a URL with the init(resolvingBookmarkData:options:relativeTo:bookmarkDataIsStale:) initializer. Many of the options map well to the options used when creating a bookmark, but at the very least, you'll need to pass .withSecurityScope. An extra parameter you'll need to pass is a var for determining when the bookmark has expired, in which you're expected to create a new bookmark from the returned URL.

For your case, it may look something like this:

struct Bookmark {
  let data: Data
  let options: URL.BookmarkCreationOptions
}

struct URLBookmark {
  let url: URL
  let bookmark: Bookmark
}
.fileImporter(isPresented: $isPresenting, allowedContentTypes: [.image, .pdf], allowsMultipleSelection: true) { result in
  switch result {
    case .success(let urls):
      let items = urls.compactMap { url -> URLBookmark? in
        let data: Data
        let options: URL.BookmarkCreationOptions = [.withSecurityScope, .securityScopeAllowOnlyReadAccess, .withoutImplicitSecurityScope]
        
        do {
          let accessing = url.startAccessingSecurityScopedResource()
          
          defer {
            if accessing {
              url.stopAccessingSecurityScopedResource()
            }
          }
          
          data = try url.bookmarkData(options: options, includingResourceValuesForKeys: [])
        } catch {
          print(error)
          
          return nil
        }
        
        return URLBookmark(url: url, bookmark: .init(data: data, options: options))
      }
      
      items.forEach { item in
        let accessing = item.url.startAccessingSecurityScopedResource()
        
        defer {
          if accessing {
            item.url.stopAccessingSecurityScopedResource()
          }
        }
        
        // Do something...
      }
    case .failure(let err):
      print(err)
  }
}

Then, when you want to create the URL from the bookmark, you could have some code like so:

let bookmarks: [Bookmark] = ...
let items = bookmarks.compactMap { bookmark -> URLBookmark? in
  var options = URL.BookmarkResolutionOptions()

  if bookmark.options.contains(.withSecurityScope) {
    options.insert(.withSecurityScope)
  }

  if bookmark.options.contains(.withoutImplicitSecurityScope) {
    options.insert(.withoutImplicitStartAccessing)
  }

  var stale = false
  let url: URL

  do {
    url = try URL(resolvingBookmarkData: bookmark.data, options: options, relativeTo: nil, bookmarkDataIsStale: &stale)
  } catch {
    print(error)

    return nil
  }

  let data: Data

  if stale {
    do {
      let accessing = url.startAccessingSecurityScopedResource()

      defer {
        if accessing {
          url.stopAccessingSecurityScopedResource()
        }
      }

      data = try url.bookmarkData(options: bookmark.options, includingResourceValuesForKeys: [])
    } catch {
      print(error)

      return nil
    }
  } else {
    data = bookmark.data
  }

  return .init(url: url, bookmark: .init(data: data, options: bookmark.options))
}

// Now do something with items...

How you store the bookmarks is up to you. It could be in a file on disk, a Core Data store, in @SceneStorage or @AppStorage, etc. I would personally suggest that if you plan on potentially storing a lot of bookmarks (say, over 200), you should stay away from the storage property wrappers as they write to UserDefaults, and bookmarks can start to take a lot of storage (1-8KB each).

like image 172
Klay Avatar answered Nov 04 '25 03:11

Klay



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!