Leveraging Realm with SwiftUI
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:
- Seamless Integration
- Real-time Data Synchronization
- Efficient CRUD Operations
- Automatic Schema Migration
- Thread-Safe and Multi-Platform Support
- Powerful Querying and Filtering
- 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. 👨🏽💻👩🏽💻
- Create a SwiftUI App and give it a name of your choice(eg RealmSwiftUI). I named mine MyContacts.
- 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()
}
}
3. Let’s add the RealmSwift dependency using the Swift Package Manager.
i. 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.