A SwiftUI example with the smallest amount of code - How to add test data to preview

Hi, I’m new to the MongoDB community with an interest in using MongoDB Realm with SwiftUI. Using some of the SwiftUI samples provided with Realm as guidance, I wanted to write an example of using Realm with the smallest amount of code. This is what I came up with (not even an HStack or padding to make it look nice):

import SwiftUI
import RealmSwift

class AutoMaker: Object, ObjectKeyIdentifiable {
    @objc dynamic var name = ""
}

struct ContentView: View {
    @State private var text = ""
    @ObservedResults(AutoMaker.self) var autoMakers
    
    var body: some View {
        TextField("Automaker Name", text: $text)
        Button(action: addItem) {
            Text("Add")
        }
        List {
            ForEach(autoMakers) {autoMaker in
                Text(autoMaker.name)
            }
            .onDelete(perform: $autoMakers.remove)
        }
    }
    
    func addItem() {       
        let maker = AutoMaker()
        maker.name = text
        $autoMakers.append(maker)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

And this is what it looks like:

SwiftUI_Small_Gif

It amazes me that I can create a working sample that adds and deletes items from a database with less than 20 lines of added code.

I do have a question however. In keeping with the theme of writing the smallest amount of code, if I want to add sample data to show up in the Preview window, what is the simplest way to do that? I found code in Andrew Morgan’s RChat example where there is a Realm.bootstrap() function called as the first line in PreviewProvider code (here). The function loads a realm database before the preview code is called. In that example, each Realm object class implements a protocol called Samplable in an extension and then provides sample data.

Is this the simplest way to test a SwiftUI view that uses Realm? Or is there a briefer way?

Thanks!
–Tom

1 Like

Thanks for the little code! That will be very useful to a lot of people that just getting started.

It sounds like you’re asking for a pre-populated Realm with test data. Is that correct? One option is to bundle a Realm with your app and then make a copy of it. Bundled Realms are read only so the copy would then provide read/write access.

There’s a section in the legacy Realm Documentation about Bundling a Realm.

Alternately on app start you can query data or the Realm file and if it doesn’t exist, create some realm objects and write them.

If that’s not what you’re asking, can you clarify?

Actually I was referring to providing data to be used in the Preview area of XCode. So test Realm data to be used in this part of my code:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I want to be able to add test data to the PreviewProvider so that I can work on my app using the Preview canvas, which will allow me to work without having to build and run the app each time I make a change to a view. In the RChat app, test data is provided to the Preview area of some views by calling Realm.bootstrap():

struct AuthorView_Previews: PreviewProvider {
    static var previews: some View {
        Realm.bootstrap()
        
        return AppearancePreviews(AuthorView(userName: "rod@contoso.com"))
            .previewLayout(.sizeThatFits)
            .padding()
    }
}

That bootstrap function on Realm was added as an extension (the RChat source code is here), and looks like this:

extension Realm: Samplable {
    static var samples: [Realm] { [sample] }
    static var sample: Realm {
        let realm = try! Realm()
        try! realm.write {
            realm.deleteAll()
            User.samples.forEach { user in
                realm.add(user)
            }
            Chatster.samples.forEach { chatster in
                realm.add(chatster)
            }
            ChatMessage.samples.forEach { message in
                realm.add(message)
            }
        }
        return realm
    }
    
    static func bootstrap() {
        do {
            let realm = try Realm()
            try realm.write {
                realm.deleteAll()
                realm.add(Chatster.samples)
                realm.add(User(User.sample))
                realm.add(ChatMessage.samples)
            }
        } catch {
            print("Failed to bootstrap the default realm")
        }
    }
}

So I’m just wondering if there is a simpler way to provide test Realm data to the Preview window (I’m trying to figure out the simplest way). Or would I need to create a similar bootstrap method?

Hi @TomF, love your sample app!

How to work with SwiftUI (Canvas) previews is a bit of an ongoing obsession of mine. I find them incredibly useful, but frustrating at times!

When they’re set up, being able to see a live preview of any view (including light and dark modes simultaneously) is great. Trying to debug them, not so much – unexplained, opaque error messages and no ability to add breakpoints or even print to the console. It’s worth remembering that these previews are just another view, and so I will sometimes replace my ContentView with the contents of one of my previews so that I can debug it.

I’m looking forward to seeing what other ideas people have to work with Realm data (especially when working with Realm Sync and partitions).

If you want to provide Realm backing data to SwiftUI, an in-memory Realm is a super simple approach and a minimal amount of code. This can be used with Swift or SwiftUI as Realm is the backing data is a separate from the UI itself.

Here’s a quickie example to populate an in memory realm with two users, two chat and two messages

let app: RealmSwift.App? = nil
var gRealm: Realm! = nil

struct ContentView: View {
    @ObservedResults(Your_observed_model.self) var myModel
    @State var navigationViewIsActive: Bool = false
    
    init() {
        gRealm = try! Realm(configuration: Realm.Configuration(inMemoryIdentifier: "MyInMemoryRealm"))

        let user0 = UserClass(name: "Jay")
        let user1 = UserClass(name: "Leroy J.")
        let chat0 = ChatsterClass(chat: "chat 0")
        let chat1 = ChatsterClass(chat: "chat 1")
        let message0 = MessageClass(msg: "Hello, World")
        let message1 = MessageClass(msg: "To the moon!")

        try! self.realm.write {
            gRealm.add([user0, user1, chat0, chat1, message0, message1])
        }
    }
    ...

The above code would run at app start and would make those objects available throughout the app. You could add, edit and remove from the in memory realm just like an on disk or sync’d realm.

I think that’s about as simple as it can get!

1 Like

Thanks @Jay , I like the idea of using an in-memory Realm for adding some test code to the running app. However, what I was trying to figure out here is the simplest way to add some test data to Previews. In the WWDC 2020 presentation “Introduction to Swift” (here), the presenter, Jacob Xiao, builds up a sample Sandwich app over the course of the presentation. At the very end he says, “But there’s one last thing that I want to point out, and it’s something we didn’t see. We just built up this entire application and tested all of these rich behaviors without ever once building and running our app. Xcode Previews let us view, edit, and debug our application much faster than was ever possible before.”

So in the spirit of trying to write a sample SwiftUI Realm app with the least amount of code, I wanted to also provide some test data to the Previews window with the least amount of code. That way I could continue building up the app without ever hitting the Build and Run button.

I looked at Andrew’s RChat example some more and figured out how to add the test code directly to the Preview code:

struct ContentView_Previews: PreviewProvider {
    
    static var previews: some View {
        createData()
        return ContentView()
    }
    
    static func createData() {
        let realm = try! Realm()
        try! realm.write {
            realm.deleteAll()
            realm.add(AutoMaker(name: "Ford"))
            realm.add(AutoMaker(name: "Honda"))
            realm.add(AutoMaker(name: "Volkswagen"))
        }
    }
}

This worked to provided Realm data for the Preview window:

SwiftUIPreviewWindow

What surprised me with this code is that the Realm sample data in the Previews window is independent of the data I add to the running app. I thought they both might be using the same default Realm instance and therefore might interfere with one another (the Previews code might erase what I add to the running code), but they don’t, which is great.

Thanks also @Andrew_Morgan for the insights into using Previews. I like the tip on how you can debug Preview code. Hopefully Apple will introduce some improvements to debugging Previews in June. Yea, dealing with Realm Sync and partitions will be another level up.

Here is my final code for creating a working Realm SwiftUI app with the least amount of code:

import SwiftUI
import RealmSwift

class AutoMaker: Object, ObjectKeyIdentifiable {
    @objc dynamic var name = ""
    
    convenience init (name: String) {
        self.init()
        self.name = name
    }
}

struct ContentView: View {
    @State private var text = ""
    @ObservedResults(AutoMaker.self) var autoMakers
    
    var body: some View {
        VStack {
            HStack {
                TextField("Automaker Name", text: $text)
                Button(action: addItem) {
                    Text("Add")
                }
            }
            List {
                ForEach(autoMakers) {autoMaker in
                    Text(autoMaker.name)
                }
                .onDelete(perform: $autoMakers.remove)
            }
        }
        .padding()
    }
    
    func addItem() {
        $autoMakers.append(AutoMaker(name: text))
        text = ""
    }
}

struct ContentView_Previews: PreviewProvider {
    
    static var previews: some View {
        createData()
        return ContentView()
    }
    
    static func createData() {
        let realm = try! Realm()
        try! realm.write {
            realm.deleteAll()
            realm.add(AutoMaker(name: "Ford"))
            realm.add(AutoMaker(name: "Honda"))
            realm.add(AutoMaker(name: "Volkswagen"))
        }
    }
}

Thanks all for the help.
–Tom

3 Likes

One more thing I should add. For those wanting to run the code above, here are the complete steps:

  1. Create a new SwiftUI app.
  2. Replace the code in ContentView.swift with the code above.
  3. Add in the Realm dependency using the Swift Packages Manager. In XCode, go to File | Swift Package | Add Package Dependency, and paste in: GitHub - realm/realm-swift: Realm is a mobile database: a replacement for Core Data & SQLite
  4. Hit Run.

You now have a working SwiftUI app that persists data to a Realm database (using realm-cocoa version 10.7.2).

1 Like

Does this work reliably for anyone at all? When I try to add mock data in the preview, if I run the preview with the bootstrap method, I get an error. But then I take out the bootstrap method, and the mock data is there.

RemoteHumanReadableError: Failed to update preview.

The preview process appears to have crashed.

Error encountered when sending 'display' message to agent.

==================================

|  RemoteHumanReadableError: The operation couldn’t be completed. (BSServiceConnectionErrorDomain error 3.)
|  
|  BSServiceConnectionErrorDomain (3):
|  ==BSErrorCodeDescription: OperationFailed

Hi @Lukasz_Ciastko,

that preview is working for me

In general, I find Canvas previews one of the coolest and most frustrating/unreliable features of SwiftUI. I keep hoping that the next version will be more robust (or at least come with some debugging features) – perhaps there will be good news at the next WWDC!

2 Likes