Did you ever want your app to open the last used document on startup? Well, that’s not too difficult on macOS. In your NSDocumentController
subclass you have the array of recentDocumentURLs
, and on startup you can try to open the first member if it exists.
But on iOS it’s a little bit more tricky. There is no such array as on macOS. So we have to mimic it by ourself.
This is a review of my post 2 years ago: UIDocumentBrowserViewController and External Storage
If you have a document based app on iOS you probably want it to be able to open documents outside the sandbox. To do so you may have to set some flags in the info file of the app, esp.:
Application supports iTunes file sharing YES
Supports opening documents in place YES
And if you want your app to store documents on the iCloud you have to give the corresponding entitlement in “Signing & Capabilities”:
iCloud: ✅ iCloud documents
Well, now we can store the last opened document URL in our User Defaults whenever we open or create a new document:
1 2 3 4 5 6 7 8 9 10 |
static let lastGameKey = "lastGame" // the key to store last game url in user defaults … private func saveLastFile(ofURL url: URL) { if url.startAccessingSecurityScopedResource(), // try to access the possibly secured url let bookmark = try? url.bookmarkData(options: [], includingResourceValuesForKeys: nil, relativeTo: nil) { // and create a bookmark of it UserDefaults.standard.set(bookmark, forKey: DocumentBrowserViewController.lastGameKey) // write it url.stopAccessingSecurityScopedResource() // and stop secure access } } |
We should never store a path or something like that if we want to re-open a document later. Instead, we create a bookmark of the URL and store that data object in the User Defaults.
Now we can try to open that stored URL on startup of our app, e.g. at viewDidLoad of our document controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
override func viewDidLoad() { super.viewDidLoad() … // Do any additional setup after loading the view, typically from a nib. // did we already open a game before? var stale = false if let bookmark = UserDefaults.standard.data(forKey: Self.lastGameKey), let url = try? URL.init(resolvingBookmarkData: bookmark, options: [], relativeTo: nil, bookmarkDataIsStale: &stale) { // the bookmark decoding worked if let ok = try? url.checkResourceIsReachable(), ok { // and is the game still existing? presentDocument(at: url) // open it } else { … } if stale { // the bookmark is not good any more saveLastFile(ofURL: url) // save the url to user defaults } } else { … } } |
First we have to decode the bookmark stored in the User Defaults and create an URL from it. If that’s ok we can open the document. If we do not succeed we have to present an empty document or show the document browser to select another one. If the bookmark is not valid any more we can try to create a new one from the supplied URL.
That all works fine for locally stored or iCloud documents. But, funny enough, for documents on other cloud services (e.g. on Synology NAS Servers) this mechanism fails. I’ve been struggling with this issue for years now and finally gave up to solve it.
Then, recently, I stumbled over this post on Stackoverflow.
It seemed to be a little bit unrelated but I gave it a try if this trick works for me as well. And yes, asking the file manager whether the file exists on that external storage seems to force a download of the file. And I can open it despite the fact that the file manager signals that the file is not present. The file coordinator mentioned in this post is already implemented in the document classes of iOS. Asking the file manager for the file seems to make the trick. I would like to count that as bug.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
override func viewDidLoad() { super.viewDidLoad() … // Do any additional setup after loading the view, typically from a nib. // did we already open a game before? var stale = false if let bookmark = UserDefaults.standard.data(forKey: Self.lastGameKey), let url = try? URL.init(resolvingBookmarkData: bookmark, options: [], relativeTo: nil, bookmarkDataIsStale: &stale) { // the bookmark decoding worked let fileManager = FileManager.default if !fileManager.fileExists(atPath: url.path) { // force a download to the device print("FILE NOT AVAILABLE") } if let ok = try? url.checkResourceIsReachable(), ok { // and is the game still existing? presentDocument(at: url) // open it } else { … } if stale { // the bookmark is not good any more saveLastFile(ofURL: url) // save the url to user defaults } } else { … } } |
But anyway, now I’m happy with this workaround and can re-open the last used document on iOS even if it is stored on some other cloud service.