0

I've been trying to use a Realm file as a document in a Document-based iOS app. The goal is to select the Realm file from the file picker so the user can switch between Realm databases, or send a database that they've worked with to another user. Similar to a text editor creating a new text file for each new document the user creates, but with a Realm db instead of a text file.

I have the file picker showing as the initial view just fine (so I think the DocumentBrowswerViewController isn't the root of the issue), but the "+" button to create a new file doesn't actually create a new file. What am I missing to get a blank file created (with the default.realm file I included in the app bundle) or open an existing file?

My DocumentBrowserViewController class:

class DocumentBrowserViewController: UIDocumentBrowserViewController, UIDocumentBrowserViewControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        delegate = self

        allowsDocumentCreation = true
        allowsPickingMultipleItems = false
    }

    func documentBrowser(_ controller: UIDocumentBrowserViewController, didRequestDocumentCreationWithHandler importHandler: @escaping (URL?, UIDocumentBrowserViewController.ImportMode) -> Void) {
        let newDocumentURL: URL? = Bundle.main.url(forResource: "default", withExtension: "realm")

        if newDocumentURL != nil {
            importHandler(newDocumentURL, .copy)
        } else {
            importHandler(nil, .none)
        }
    }

    func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) {
        guard let sourceURL = documentURLs.first else { return }

        presentDocument(at: sourceURL)
    }

    func documentBrowser(_ controller: UIDocumentBrowserViewController, didImportDocumentAt sourceURL: URL, toDestinationURL destinationURL: URL) {
        presentDocument(at: destinationURL)
    }

    func documentBrowser(_ controller: UIDocumentBrowserViewController, failedToImportDocumentAt documentURL: URL, error: Error?) {
        // Make sure to handle the failed import appropriately, e.g., by presenting an error message to the user.
    }

    // MARK: Document Presentation

    func presentDocument(at documentURL: URL) {
        let document = try! Document(contentsOf: documentURL, ofType: "RealmDocument")

        // Access the document
        document.open(completionHandler: { success in
            if success {
                // Display the content of the document:
                let view = RootView(realmDocument: document) 

                let documentViewController = UIHostingController(rootView: view)
                self.present(documentViewController, animated: true, completion: nil)
            } else {
                // Make sure to handle the failed import appropriately, e.g., by presenting an error message to the user.
            }
        })
    }

    func closeDocument(_ document: Document) {
        dismiss(animated: true) {
            document.close(completionHandler: nil)
        }
    }
}

and the Document class (wrapping the Realm database file):

enum RealmDocumentError: Error {
    case FailedToConvertURLToFilePath
}

class Document: UIDocument {

    var representedRealm: Realm = {
        var config = Realm.Configuration()
        config.inMemoryIdentifier = NSUUID().uuidString
        do {
            return try Realm(configuration:config)
        } catch {
            fatalError("Realm init failed")
        }
    }()


    private static func realmURL(url: URL) -> URL {
        return url.appendingPathComponent(NSUUID().uuidString)
    }


    convenience init(contentsOf url: URL, ofType typeName: String) throws {
        self.init()

        do {
            try representedRealm = Realm(fileURL: url)
        } catch {
            fatalError("Something went wrong creating the realm for \(url)")
        }
    }


    convenience init(for urlOrNil: URL?, withContentsOfURL contentsURL: URL, ofType typeName: String) throws {
        self.init()

        var config = Realm.Configuration()
        config.fileURL = contentsURL
        config.readOnly = true
        let originalRealm = try Realm(configuration: config)

        if let url = urlOrNil {
            try originalRealm.writeCopy(toFile: url)
            representedRealm = try Realm(fileURL: url)
        } else {
            var temporaryInMemoryRealmURL = URL(fileURLWithPath:NSTemporaryDirectory())
            temporaryInMemoryRealmURL.appendPathComponent(NSUUID().uuidString)

            try originalRealm.writeCopy(toFile: temporaryInMemoryRealmURL)
            representedRealm = try Realm(fileURL: temporaryInMemoryRealmURL)
        }
    }

    deinit {
        // This is a workaround — ideally Realm would allow initialising an in-memory Realm based on an on-disk representation and this cleanup code would be unnecessary
        guard let url = representedRealm.configuration.fileURL else {
            return
        }
        if url.path.hasPrefix(NSTemporaryDirectory()) {
            do {
                let fileManager = FileManager.default
                try fileManager.removeItem(at: url)
            }   catch let error as NSError {
                Swift.print("Error while deleting temporary Realm: \(error.localizedDescription)")
            }
        }
    }

    override func load(fromContents contents: Any, ofType typeName: String?) throws {
        let url = fileURL
        let proposedRealmPath = url.path
        if let currentRealmPath = representedRealm.configuration.fileURL?.path {

            if currentRealmPath != proposedRealmPath {
                try! representedRealm.writeCopy(toFile: url)
                var config = Realm.Configuration()
                config.fileURL = url
                representedRealm = try! Realm(configuration: config)

                let fileManager = FileManager.default
                try! fileManager.removeItem(atPath: currentRealmPath)
            }
        } else {
            throw RealmDocumentError.FailedToConvertURLToFilePath
        }
    }


    override func writeContents(_ contents: Any, to url: URL, for saveOperation: UIDocument.SaveOperation, originalContentsURL: URL?) throws {
        try representedRealm.writeCopy(toFile: url)
    }


    override var hasUnsavedChanges: Bool {
        return representedRealm.isInWriteTransaction
    }

}
Dwillmore
  • 65
  • 1
  • 7
  • That's quite a bit of code for us to parse through and the question is a bit vague. What are *files* in this use case? What does *actually create a new file* mean - create a new Realm file? Are you trying to create a new Realm file for each user or something else? If you want to reference a Realm file, see the Realm Getting Started guide [Config a local Realm](https://realm.io/docs/swift/latest/#configuring-a-realm). It sounds like you are trying to create a new Realm for each document the user creates - is that the case? Like if they are using a word processor and each document is a Realm? – Jay Apr 07 '20 at 17:19
  • @Jay I updated the question to make it more clear what I'm looking to achieve. Your word processor comparison was spot on - the goal is to have an app that creates a new Realm for each document the user creates, so that document (and the Realm data) can be transferred between devices or users. The app I'm working on currently works with a single local Realm file, but that's limiting in that there's no good way for the user to change between different data sets. – Dwillmore Apr 07 '20 at 17:35
  • When responding, be sure to mention who you're responding to with the at symbol, like @Jay. I understand what you're doing but really, there's no reason to do that. Create a Full Sync Realm and store the documents in that Realm. You could also store multiple 'documents' in a single realm per user as well. However, with Full Sync, It will be available across devices and users and greatly simplify deployment, organization and maintenance. If you're using a local Realm, what's *preventing* changing *datasets*. On that note, what is a *dataset* in this use case? Can you clarify the question? – Jay Apr 07 '20 at 19:51
  • @Jay The dataset is data for lighting equipment for theatre, all the data contained in each object is text. I suppose you're right, it might just be easier to change datasets within the single local Realm and distributing the data between users as text or xml. So something like "opening" a file really being importing the data from the text/xml, the user making changes, and saving/closing the file really being exporting the data back to the text/xml file and clearing it from the local Realm? – Dwillmore Apr 07 '20 at 21:07
  • Not sure why you would need to go through that process. Each lighting equipment object would have properties about that object, perhaps a name and bulb type. Those objects would each be stored in a realm. Users would be able to add items or update items as needed. That would be called a Fully Sync'd realm and the data would be stored in the Realm Cloud and accessible by the users. All of that data is live so as users add objects, those would be available to all users. I dont see any reason to import/export the data or clear it. If there's a need for that, it's not apparent from the question. – Jay Apr 08 '20 at 14:46
  • Is this somehow relevant? https://github.com/realm/realm-cocoa/issues/1066 – Jay Lee May 12 '21 at 09:40

0 Answers0