Leveraging Realm with SwiftUI

Maxwell Ehiawey
10 min readJul 20, 2023

--

Realm logo and Swift logo.
Realm logo and Swift logo.

Introduction

In the fast-paced world of mobile app development, data persistence is a crucial aspect that directly impacts the user experience and the overall functionality of an application. As developers, we constantly seek efficient and reliable solutions to store and retrieve data seamlessly. This is where Realm, a robust database framework, comes into play.

In this article, we will dive into the world of Realm and explore how it can be effectively used with SwiftUI to handle data persistence. We will set up Realm in a SwiftUI project, create data models, perform CRUD operations, and leverage live objects for real-time updates.

Using realm with SwiftUI comes in handy with many advantages. Here are some key advantages of using RealmSwift with SwiftUI:

  1. Seamless Integration
  2. Real-time Data Synchronization
  3. Efficient CRUD Operations
  4. Automatic Schema Migration
  5. Thread-Safe and Multi-Platform Support
  6. Powerful Querying and Filtering
  7. Excellent Performance

By leveraging these advantages, developers can build robust and responsive SwiftUI apps that seamlessly handle data persistence with RealmSwift. It streamlines the development process, reduces complexity, and empowers developers to focus on creating delightful user experiences.

Let’s start coding. 👨🏽‍💻👩🏽‍💻

  1. Create a SwiftUI App and give it a name of your choice(eg RealmSwiftUI). I named mine MyContacts.
  2. The ContentView code will look like the one below and you’ll get the subsequent screen when the code is built.
import SwiftUI

struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Hello, World! Screen
Hello, World! Screen

3. Let’s add the RealmSwift dependency using the Swift Package Manager.

i. In XCode, go to File>Add Packages

In XCode, go to File>Add Packages>

ii. The popup search for the Realm-Swift package with github.com/realm/realm-swift.git and click on Add package below on the right bottom corner of the popup window.

iii. Wait for the verification windows to complete. It will take some time to complete depending on your internet speed.

iv. Be sure to check the Realm and RealmSwift boxes as indicated below and press Add Package.

4. Let’s create a file for the contact Model. Create a data model called ContactModel and let it inherit the Identifiable protocol. This will be the structure of the item(s) we’ll be working with.

import Foundation

struct ContactModel: Identifiable {
var id = UUID()
var name: String
var phone: String
var isBuddy: Bool = false
}

The code defines a `ContactModel` struct that represents a contact entry in an application. It conforms to the `Identifiable` protocol, indicating that it has a unique identifier.

The `ContactModel` struct has the following properties:

  • `id`: A `UUID` property that represents the unique identifier of the contact. It is initialized with a random UUID value.
    - `name`: A `String` property that stores the name of the contact.
    - `phone`: A `String` property that stores the phone number of the contact.
    - `isBuddy`: A `Bool` property that indicates whether the contact is a buddy or not. It is initialized with a default value of `false`.

5. Now let’s create A Single Contact View as shown below. The heart button toggles to switch between isBuddy or otherwise.

import SwiftUI

struct SingleContactView: View {
@State var isBuddy: Bool = false

var body: some View {
VStack(spacing: 8) {
HStack {
VStack(spacing: 8){
HStack {
Text("Name:")
Text("Kwame Nkrumah")
Spacer()
}
HStack {
Text("Phone:")
Text("+2330240000000")
Spacer()
}
.opacity(0.6)

}
Button(action: {
isBuddy.toggle()
}) {
Image(systemName: isBuddy ? "heart.fill": "heart")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor( isBuddy ? Color.green: Color.gray)
.padding(.trailing)
}
}
}
.padding(.all, 16)
}
}


// Add this to your ContentView as shown below

struct ContentView: View {
var body: some View {
VStack {
SingleContactView() //: New replacement update
}
.padding()
}
}

6. Let’s continue with the contact list design by creating a file of the list of contacts as shown below.

import SwiftUI

struct ContactListView: View {

var body: some View {
ScrollView {
LazyVStack {

}
}
}
}

// Add this to your ContentView as shown below

struct ContentView: View {
var body: some View {
VStack {
ContactListView() //: New replacement update
}
.padding()
}
}
// putting multiples of the SinglecontactView 

import SwiftUI

struct ContactListView: View {

var body: some View {
ScrollView {
LazyVStack {
SingleContactView()
SingleContactView()
SingleContactView()
SingleContactView()
SingleContactView()
SingleContactView()
SingleContactView()
SingleContactView()
SingleContactView()
SingleContactView()
}
}
}
}
// this will result in the build below;
// good for a simple contact list view design

4. Tidying up this code and making it more mature will result as below.

// let's replace single contact view with the code below

struct SingleContactView: View {
@State var isBuddy: Bool = false
var contact: ContactModel
var body: some View {
VStack(spacing: 8) {
HStack {
VStack(spacing: 8){
HStack {
Text("Name:")
Text(contact.name)
Spacer()
}
HStack {
Text("Phone:")
Text(contact.phone)
Spacer()
}}
Button(action: {
isBuddy.toggle()
}) {
Image(systemName: isBuddy ? "heart.fill": "heart")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor( isBuddy ? Color.green: Color.gray)
.padding(.trailing)
}
}
}
.padding(.all, 16)
}
}


// Creating generic textfield design called NiceTextFieldStyle

struct NiceTextFieldStyle: TextFieldStyle {
func _body(configuration: TextField<_Label>) -> some View {
configuration
.padding(10)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.gray, lineWidth: 2)
)
}
}

// Creating generic button design called NiceButtonStyle

struct NiceButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(16)
.background(Color.gray)
.foregroundColor(.white)
.cornerRadius(8)
.font(.headline)
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.animation(.easeInOut, value: 1.0)
}
}


// Creating a sinple contact detail page

struct ContactDetail: View {
@State private var name: String = ""
@State private var phone: String = ""
var contact: ContactModel
var body: some View {
VStack(alignment: .center, spacing: 24) {
VStack(alignment: .leading, spacing: 12) {
Text("Name")
.foregroundColor(Color.gray)
TextField("Enter Name", text: $name)
.font(.title3)
Divider()
}
VStack(alignment: .leading, spacing: 12) {
Text("Phone")
.foregroundColor(Color.gray)
TextField("Enter title..", text: $phone)
.font(.title3)
Divider()
}
VStack(alignment: .center, spacing: 12) {
Button(action: {

}) {
HStack {
Image(systemName: "trash")
.foregroundColor(.red)
Text("Delete")
.foregroundColor(.red)
}
}
.buttonStyle(NiceButtonStyle())
}
Spacer()
}
.onAppear{
name = contact.name
phone = contact.phone
}
.navigationBarItems(
trailing: Button(action: {

}) {
Text("Save")
}
)
.navigationBarTitle("Edit Todo", displayMode: .inline)
.padding(24)
}
}

// Creating a simple Add contact view

struct AddContactView: View {

@State private var name: String = ""
@State private var phone: String = ""
@Environment(\.dismiss) var dismiss

var body: some View {
VStack(spacing: 20) {

Text("Enter contact details")
TextField("Enter Name", text: $name)
.textFieldStyle(NiceTextFieldStyle())
TextField("Enter Phone Number", text: $phone)
.textFieldStyle(NiceTextFieldStyle())

Button(action: submitContact) {
Text("Add Contact")
.foregroundColor(.orange)
}
.buttonStyle(NiceButtonStyle())
}
.padding(20)
}

private func submitContact() {
dismiss()
}
}


// A Modified version of the ContactListView will be

struct ContactListView: View {
@State private var scale: CGFloat = 1.0

var contactList = [
ContactModel(name: "Kwame Nkrumah", phone: "+233024000000", isBuddy: true),
ContactModel(name: "Vivie Ametor", phone: "+2330240000001"),
ContactModel(name: "Dela Yokpor", phone: "+2330240000002"),
ContactModel(name: "Ama Adzee", phone: "+2330240000004", isBuddy: true),
ContactModel(name: "Salifu Anda", phone: "+2330240000005"),
ContactModel(name: "Naa Amorkor", phone: "+2330240000006"),
ContactModel(name: "Kojo Dzato", phone: "+2330240000007")

]
@State var isDetailRoute: Bool = false
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
LazyVStack (alignment: .leading) {
// 2
ForEach(contactList, id: \.id) { contact in
SingleContactView(contact: contact)
.navigationDestination(isPresented: $isDetailRoute) {
ContactDetail(contact: contact)
}
isDetailRoute.toggle()
}
Divider().padding(.leading, 20)

}
.scaleEffect(scale)
.animation(.easeInOut, value: scale)
}
}

}
}

// Modify your ContentView to look like this👇🏽👇🏽👇🏽

struct ContentView: View {
@State var isPresentingSheet: Bool = false
var body: some View {
NavigationStack {
VStack {
ContactListView()
}
.padding()
.navigationTitle("My Contacts")
.navigationBarTitleDisplayMode(.automatic)
.sheet(isPresented: $isPresentingSheet) {
AddContactView()
}
.navigationBarItems(
trailing: Button(action: {
isPresentingSheet.toggle()
}) {
Image(systemName: "person.badge.plus")
Text("Add")
}
)

}
}
}

// ☝🏽☝🏽☝🏽 Remember to keep all your imports intact ☝🏽☝🏽☝🏽

//Congrat! 👏🏽👏🏽👏🏽 So far so good. Up this point is fine for a basic UI design
// We'll use this to implement our the RealSwift CRUD.

The above modifications yield a UI design with a list of contacts and a button above to add a contact by presenting a sheet. Clicking any contact leads to a contact detail page.

6. Hurray!!! Let’s now dive into the Realm Code👨🏽‍💻👨🏽‍💻. Create a file, Import RealmSwift and add a Realm data object similar to what we had and Update the ContactModel as shown below.

import RealmSwift

class ContactObject: Object, Identifiable {
@Persisted(primaryKey: true) var id: ObjectId
@Persisted var name: String
@Persisted var phone: String
@Persisted var isBuddy: Bool = false
}


// Newly Updated ContactModel

struct ContactModel: Identifiable {
var id: String
var name: String
var phone: String
var isBuddy: Bool = false

init(contact: ContactObject) {
self.id = contact.id.stringValue
self.name = contact.name
self.phone = contact.phone
self.isBuddy = contact.isBuddy
}
}

7. Let's add a viewModel file to implement our CRUD.

class ContactsViewModel: ObservableObject {

@ObservedResults(ContactObject.self) var contactLists
@Published var contacts: [ContactModel] = []

private var token: NotificationToken?

init() {
setupObserver()
}

deinit {
token?.invalidate()
}
// fetch and update contactList
private func setupObserver() {
do {
let realm = try Realm()
let results = realm.objects(ContactObject.self)

token = results.observe({ [weak self] changes in
self?.contacts = results.map(ContactModel.init)
.sorted(by: { $0.name > $1.name })
})
} catch let error {
print(error.localizedDescription)
}
}
// Add contact
func addContact(name: String, phone: String, isBuddy: Bool) {
let contact = ContactObject()
contact.name = name
contact.phone = phone
contact.isBuddy = isBuddy
$contactLists.append(contact)
}

// Delete contact
func remove(id: String) {
do {
let realm = try Realm()
let objectId = try ObjectId(string: id)
if let contact = realm.object(ofType: ContactObject.self, forPrimaryKey: objectId) {
try realm.write {
realm.delete(contact)
}
}
} catch let error {
print(error)
}
}
// Update contact
func update(id: String, name: String, phone: String, isBuddy: Bool) {
do {
let realm = try Realm()
let objectId = try ObjectId(string: id)
let contact = realm.object(ofType: ContactObject.self, forPrimaryKey: objectId)
try realm.write {
contact?.name = name
contact?.phone = phone
contact?.isBuddy = isBuddy
}
} catch let error {
print(error.localizedDescription)
}
}
}

8. Finally, let’s update the rest of the files.


// ContactModel

struct ContactModel: Identifiable {
var id: String
var name: String
var phone: String
var isBuddy: Bool = false

init(contact: ContactObject) {
self.id = contact.id.stringValue
self.name = contact.name
self.phone = contact.phone
self.isBuddy = contact.isBuddy
}
}
// ContactObject
class ContactObject: Object, Identifiable {
@Persisted(primaryKey: true) var id: ObjectId
@Persisted var name: String
@Persisted var phone: String
@Persisted var isBuddy: Bool
}

// Single Contact file

struct SingleContactView: View {
@State private var isBuddy: Bool = false
@ObservedObject private var viewModel = ContactsViewModel()
var contact: ContactModel
var body: some View {
VStack(spacing: 8) {
HStack {
VStack(spacing: 8){
HStack {
Text("Name:")
Text(contact.name)
Spacer()
}
HStack {
Text("Phone:")
Text(contact.phone)
Spacer()
}}
Button(action: {
isBuddy.toggle()
viewModel.update(id: contact.id, name: contact.name, phone: contact.phone, isBuddy: isBuddy)
}) {
Image(systemName: contact.isBuddy ? "heart.fill": "heart")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor( contact.isBuddy ? Color.green: Color.gray)
.padding(.trailing)
}
}
}
.onAppear{
isBuddy = contact.isBuddy
}
.padding(.all, 16)
}
}

// ContactList File
struct ContactListView: View {

@State private var scale: CGFloat = 1.0
@ObservedObject private var viewModel = ContactsViewModel()

@State var isDetailRoute: Bool = false
var body: some View {
ScrollView {
LazyVStack (alignment: .leading) {
ForEach(viewModel.contacts, id: \.id) { contact in
SingleContactView(contact: contact)
.onTapGesture {
isDetailRoute.toggle()
}
.navigationDestination(isPresented: $isDetailRoute) {
ContactDetailView(contact: contact)
}
Divider().padding(.leading, 20)
}
}

}
}
}

// AddContactView file
struct AddContactView: View {
@State private var viewModel = ContactsViewModel()
@State private var name: String = ""
@State private var phone: String = ""
@State private var isBuddy: Bool = false
@Environment(\.dismiss) var dismiss

var body: some View {
VStack(spacing: 20) {

Text("Enter contact details")
TextField("Enter Name", text: $name)
.textFieldStyle(NiceTextFieldStyle())
TextField("Enter Phone Number", text: $phone)
.textFieldStyle(NiceTextFieldStyle())
Toggle("My Buddy?", isOn: $isBuddy)
Button(action: submitContact) {
Text("Add Contact")
.foregroundColor(.orange)
}
.buttonStyle(NiceButtonStyle())
}
.padding(20)
}

private func submitContact() {
viewModel.addContact(name: name, phone: phone, isBuddy: isBuddy)
dismiss()
}
}

// ContactDetailView file
struct ContactDetailView: View {
@Environment(\.presentationMode) var presentationMode
@ObservedObject private var viewModel = ContactsViewModel()
@State private var name: String = ""
@State private var phone: String = ""
@State private var isBuddy: Bool = false
var contact: ContactModel
var body: some View {
VStack(alignment: .center, spacing: 24) {
VStack(alignment: .leading, spacing: 12) {
Text("Name")
.foregroundColor(Color.gray)
TextField("Enter Name", text: $name)
.font(.title3)
Divider()
}
VStack(alignment: .leading, spacing: 12) {
Text("Phone")
.foregroundColor(Color.gray)
TextField("Enter title..", text: $phone)
.font(.title3)
Toggle("My Buddy?", isOn: $isBuddy)
Divider()
}
VStack(alignment: .center, spacing: 12) {
Button(action: {
viewModel.remove(id: contact.id)
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "trash")
.foregroundColor(.red)
Text("Delete")
.foregroundColor(.red)
}
}
.buttonStyle(NiceButtonStyle())
}
Spacer()
}
.onAppear{
name = contact.name
phone = contact.phone
isBuddy = contact.isBuddy
}
.navigationBarItems(
trailing: Button(action: {
viewModel.update(id: contact.id, name: name, phone: phone, isBuddy: isBuddy)
presentationMode.wrappedValue.dismiss()
}) {
Text("Save")
}
)
.navigationBarTitle("Edit Todo", displayMode: .inline)
.padding(24)
}
}

You can see the complete code here on my Github.

--

--