Some time ago I’ve ported the old and ancient strategy game „Empire“ from macOS to iOS. On macOS I decided to make the game document based. Since the game may last many hours (or even more), it is necessary to store the state of the game in some kind of document to continue it in a later session. On start-up of the game it tries to re-open the last used document. That’s quite easy with the recentDocumentURLs
array of the NSDocumentController
.
But for iOS such a facility is missing. There is no such thing as a „UIDocumentController“ and the UIDocumentBrowserViewController
tells you nothing about recently opened or created documents. I wanted to make the iOS version of Empire document based as well to enable anytime switches between the iOS and macOS version of the game. That’s especially interesting, if the game document is stored on some external server (like iCloud).
Well, that can’t be too difficult, I thought. Since some time it’s possible to develop document based apps for iOS and with the aforementioned UIDocumentBrowserViewController
you have the grounds to create or pick documents to open in your app. I tried the following:
- On creation or on open an existing document I stored the URL supplied from the methods of the
UIDocumentBrowserViewController
in the user defaults. - On start-up of the app I looked if the user defaults for the last opened document were present.
- If they are, I looked if the stored URL was still reachable.
- If yes, I opened that document; otherwise I presented the document browser to open or create a new game.
That worked fine for locally stored documents but failed for documents stored on some cloud service (e.g. iCLoud, DropBox, ownCLoud/NextCLoud). The file system manager always told me that the resource was unreachable although the browser showed that the document was still there. Via the document browser I could open the document on the external storage without problems.
If nothing helps ask stackoverflow.com. So I did (https://stackoverflow.com/questions/56324045/how-to-reopen-a-uidocument-on-icloud-using-uidocumentbrowserviewcontroller). The solution works with bookmarks. Instead of storing the url of the last opened document directly I first make a bookmark from it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func documentBrowser(_ controller: UIDocumentBrowserViewController, didImportDocumentAt sourceURL: URL, toDestinationURL destinationURL: URL) { saveLastFile(ofURL: destinationURL) // save the url to user defaults // Present the Document View Controller for the new newly created document presentDocument(at: destinationURL) } 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 } } |
As an example this listing shows the callback of the UIDocumentBrowserViewController for document creation. To save the supplied url in the user defaults I signal the system that I am going to access a security scoped resource first. This is necessary if the url points to an external storage; otherwise the creation of the bookmark would fail. If the creation of the bookmark succeeds I store it in the user defaults.
On start-up I do the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
override func viewDidLoad() { super.viewDidLoad() delegate = self allowsDocumentCreation = true allowsPickingMultipleItems = false // 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: DocumentBrowserViewController.lastGameKey), let url = try? URL.init(resolvingBookmarkData: bookmark, options: [], relativeTo: nil, bookmarkDataIsStale: &stale), // the bookmark decoding worked let ok = try? url.checkResourceIsReachable(), ok { // and is the game still existing? if stale { // the bookmark is not good any more saveLastFile(ofURL: url) // save the url to user defaults } presentDocument(at: url) // open it } else { performSegue(withIdentifier: Identifiers.startInfo, sender: self) // display info screen } } |
In the viewDidLoad
I look for the user defaults of the last opened document. If it’s there I try to convert it to a url and look if the file is still reachable. If everything’s ok I open that document; otherwise I present the document browser. Funny enough, here I do not need to enclose the access to the url in the pair url.startAccessingSecurityScopedResource() / url.stopAccessingSecurityScopedResource()
.
Btw, the promise that the bookmark stays valid if the document is moved or renamed seems to be broken on iOS.