Using Swift data for data persistence in iOS 17 UIKit

Sai Balaji
7 min readJun 17, 2023

--

In WWDC 2023 Apple introduced a new way to add data persistence to iOS apps using Swift data. Swift data makes it easier to add data persistence to application through declarative code and it is much simpler than CoreData. It works great with SwiftUI it can also be used with UIKit.

Requirements

  • Xcode 15 Beta with iOS 17 SDK

Note: At the time of writing this blog swift data is still in beta and supports iOS 17+ apps

We are going to build a simple To-do list app which has data persistence using Swift data and perform CRUD operation on the data.

Defining our Schema

We can define our schema using Modelmacro. This macro converts a Swift class into a stored model that’s managed by SwiftData. It is simple as adding the Model macro to existing Swift data model and the schema is generated. Create a new swift file and add the following code.


import Foundation
import SwiftData

@Model
class TodoModel{
@Attribute(.unique) var id: String
var taskname: String
var time: Double

init(id: String, taskname: String,time: Double) {
self.id = id
self.taskname = taskname
self.time = time
}

}

Here we create a data model for our Todo app. Each task will have a task name, time stamp(to sort the task based on time they are added) and an unique identifier which is a string. Adding Model macro to Swift class will transform the stored properties of the class to persisted properties. It can simple swift types like Int, String etc. And also complex types like Codable, Struct,Enums and collection types. We can influence how Swift data build your schema using meta data property. Here we have used Attribute meta-data with unique to define that each task will have a unique string value.

Model Container and Model Context

Create a new Swift file and add the following code.

import Foundation
import SwiftData

class DatabaseService{
static var shared = DatabaseService()
var container: ModelContainer?
var context: ModelContext?

init(){
do{
container = try ModelContainer(for: [TodoModel.self])
if let container{

context = ModelContext(container)

}


}
catch{
print(error)
}
}
}

Here we create a singleton class named DatabaseService which will have methods to perform CRUD operations. First we create a static instance for the class. Then we create two properties of type ModelContainer and ModelContext respecitively. The model container is used to manage the app schema and app storage configuration. The ModelContext enables you to fetch, insert, and delete models, and save any changes to disk.

Saving Data

To save a data in Swift data add the following code inside DatabaseService class.

  func saveTask(taskName: String?){
guard let taskName else{return }
if let context{
let taskToBeSaved = TodoModel(id: UUID().uuidString, taskname: taskName, time: Date().timeIntervalSince1970)
context.insert(object: taskToBeSaved)
}
}

Here the function takes a String parameter which is the name of the task to be saved. Then we use it to create an object for our data model/schema with an unique id string generated using UUID and a UNIX time stamp. Finally we save the data using insert() method of the ModelContext object.

Reading Data

Inside DatabaseService class add the following method.

  func fetchTasks(onCompletion:@escaping([TodoModel]?,Error?)->(Void)){
let descriptor = FetchDescriptor<TodoModel>(sortBy: [SortDescriptor<TodoModel>(\.time)])
if let context{
do{
let data = try context.fetch(descriptor)
onCompletion(data,nil)
}
catch{
onCompletion(nil,error)
}
}
}

Here we read the data by creating an object of type FetchDescriptor which can be seen as a Swift friendly version of NSFetchDescriptor using in CoreData. This describes the criteria, sort order, and any additional configuration to use when performing a fetch operation. It takes an array of type SortDescriptor as a parameter which is a serializable description of how to sort numerics and strings. In our case we need to fetch our data in sorted in ascending order based on UINIX time stamp. Then we perform the read operation using fetch() method of the ModelContext this method can throw an exception so we surround them in do-try-catch block.

Update Data

To perform update operation add the following code in DatabaseService class

 func updateTask(task: TodoModel,newTaskName: String){
let taskToBeUpdated = task
taskToBeUpdated.taskname = newTaskName
}

Here we just take the Task object which is to be updated with a new task name and modify the task name thats all. Swift data will automatically update the underlying data.

Delete Data

To perform delete operation add the following code in Database service class.

  func deleteTask(task: TodoModel){
let taskToBeDeleted = task
if let context{
context.delete(taskToBeDeleted)
}
}

Here we just call delete() method of ModelContext and pass the task to be deleted, which will automatically delete the data from the persistence storage.

Creating App UI

In Main.story board create an UI similar to the one shown below.

In ViewController.swift file create the IBOutlets and implement the delegate and datasource methods for the tableview. Then add a right bar button item to the navigation bar and a selector method to which calls the insert method of the database service.

 func configureUI(){
title = "Swift Data Demo"
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Add", style: .plain, target: self, action: #selector(addItem))
}
  @objc func addItem(){
let avc = UIAlertController(title: "Info", message: "Add new item", preferredStyle: .alert)
avc.addTextField()
avc.addAction(UIAlertAction(title: "Ok", style: .default, handler: { action in
if let taskName = avc.textFields?.first?.text{
DatabaseService.shared.saveTask(taskName: taskName)
self.fetchData()
}
}))
self.present(avc, animated: true, completion: nil)
}

Then we create another method to fetch the data from database serviceand reload the table. We then call that method inside viewDidLoad() as our app has only single screen.

func fetchData(){
DatabaseService.shared.fetchTasks { data , error in
if let error{
print(error)
}
if let data{
self.tasks = data
DispatchQueue.main.async {
self.taskTableView.reloadData()
}
}
}
}

Then we define swipe action for the tableview rows using tableView(_:editActionsForRowAt:) method.

  func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .default, title: "Delete") { _, indexpath in
DatabaseService.shared.deleteTask(task: self.tasks[indexpath.row])
self.fetchData()
}

let update = UITableViewRowAction(style: .default, title: "Update") { _, indexpath in
let avc = UIAlertController(title: "Info", message: "Add new item", preferredStyle: .alert)
avc.addTextField()
avc.addAction(UIAlertAction(title: "Ok", style: .default, handler: { action in
if let textfield = avc.textFields?.first?.text{

DatabaseService.shared.updateTask(task: self.tasks[indexpath.row], newTaskName: textfield)
self.fetchData()
}
}))
self.present(avc, animated: true, completion: nil)
}

update.backgroundColor = UIColor.green
return [delete,update]
}

Here we create two swipe actions with their respective actions of Delete and Update. After Updating and Deleting the data from datasource we fetch the updated data and again reload the tableview.

Here is the entire ViewController.swift code



import UIKit
import SwiftData
class ViewController: UIViewController {

@IBOutlet weak var taskTableView: UITableView!
var container: ModelContainer?
var context: ModelContext?
var tasks = [TodoModel]()

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
taskTableView.delegate = self
taskTableView.dataSource = self
taskTableView.register(UITableViewCell.self,forCellReuseIdentifier: "CELL")

configureUI()
self.fetchData()
}

func configureUI(){
title = "Swift Data Demo"
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Add", style: .plain, target: self, action: #selector(addItem))
}


func fetchData(){
DatabaseService.shared.fetchTasks { data , error in
if let error{
print(error)
}
if let data{
self.tasks = data
DispatchQueue.main.async {
self.taskTableView.reloadData()
}
}
}
}


@objc func addItem(){
let avc = UIAlertController(title: "Info", message: "Add new item", preferredStyle: .alert)
avc.addTextField()
avc.addAction(UIAlertAction(title: "Ok", style: .default, handler: { action in
if let taskName = avc.textFields?.first?.text{


DatabaseService.shared.saveTask(taskName: taskName)
self.fetchData()
}
}))
self.present(avc, animated: true, completion: nil)
}
}

extension ViewController: UITableViewDelegate, UITableViewDataSource{

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tasks.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CELL", for: indexPath)
cell.textLabel?.text = "\(self.tasks[indexPath.row].taskname)"
return cell
}

func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .default, title: "Delete") { _, indexpath in


DatabaseService.shared.deleteTask(task: self.tasks[indexpath.row])
self.fetchData()

}
let update = UITableViewRowAction(style: .default, title: "Update") { _, indexpath in
let avc = UIAlertController(title: "Info", message: "Add new item", preferredStyle: .alert)
avc.addTextField()
avc.addAction(UIAlertAction(title: "Ok", style: .default, handler: { action in
if let textfield = avc.textFields?.first?.text{

DatabaseService.shared.updateTask(task: self.tasks[indexpath.row], newTaskName: textfield)
self.fetchData()
}
}))
self.present(avc, animated: true, completion: nil)
}
update.backgroundColor = UIColor.green
return [delete,update]
}

}

The project repo can be found here

Reference

https://developer.apple.com/documentation/swiftdata/

--

--

Sai Balaji
Sai Balaji

Responses (1)