Skip to main content

How to use SwiftData with UIKit

About 3 minSwiftArticle(s)bloghackingwithswift.comcrashcourseswiftswiftdataxcodeappstore

How to use SwiftData with UIKit 관련

SwiftData by Example

Back to Home

How to use SwiftData with UIKit | SwiftData by Example

How to use SwiftData with UIKit

Updated for Xcode 15

Although SwiftData was designed with SwiftUI in mind, it works great with UIKit too. However, the same drawbacks apply as when using SwiftData with MVVM in SwiftUI: you're responsible for fetching and syncing data yourself.

With UIKit the smartest approach is to use a diffable data source, so that UIKit handles table or collection view updates for you. It's then your job to update your SwiftData object list whenever something important changes, so that UIKit can sync those changes to your layout.

Let's look at a complete code sample.

First, we need a model to work with, so here's a simple one that tracks the name and birth year of a user:

import SwiftData

@Model
class User {
    var name: String
    var birthYear: Int

    init(name: String, birthYear: Int) {
        self.name = name
        self.birthYear = birthYear
    }
}

Next, we need to define the sections we'll please in or list. We only have one here, but we still need to define it:

enum Section {
    case users
}

Now add some properties to your UIViewController: one to store the active model container, one to store a collection view, and one to store your data source:

var container: ModelContainer?

var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section, User>?

Tips

I'll be using a collection view in list mode because it provides maximum flexibility with styles, but this approach works equally well with table views.

Now we can add two methods to the view controller: one to create the collection view, making sure it expands to fill the full screen, and another to create the diffable data source, showing it how to display users the way we want.

Add these two now:

func createCollectionView() {
    let configuration = UICollectionLayoutListConfiguration(appearance: .plain)
    let layout = UICollectionViewCompositionalLayout.list(using: configuration)

    collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
    collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    view.addSubview(collectionView)

    collectionView.register(UICollectionViewListCell.self, forCellWithReuseIdentifier: "User")
}

func createDataSource() {
    dataSource = UICollectionViewDiffableDataSource<Section, User>(collectionView: collectionView) { collectionView, indexPath, user in
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "User", for: indexPath)

        var content = UIListContentConfiguration.cell()
        content.text = "\(user.name) was born in \(user.birthYear)."
        cell.contentConfiguration = content

        return cell
    }
}

Both of those need to be called when our view is loaded, which means writing a loadView() method like this one:

override func loadView() {
    super.loadView()

    createCollectionView()
    createDataSource()
}

So far this is almost entirely vanilla UIKit code, but this next part is where SwiftData starts to come in: we need a method that loads all the users from our model container, places them into a diffable data snapshot, then applies that to our data source so our collection view updates

Add this now:

func loadUsers() {
    let descriptor = FetchDescriptor<User>()
    let users = (try? container?.mainContext.fetch(descriptor)) ?? []

    var snapshot = NSDiffableDataSourceSnapshot<Section, User>()
    snapshot.appendSections([.users])
    snapshot.appendItems(users)
    dataSource?.apply(snapshot, animatingDifferences: false)
}

Tips

If you want to add sorting or filtering, you need to do it in the initializer for FetchDescriptor.

To finish up, we need to provide a viewDidLoad() method that creates our SwiftData model container and calls loadUsers() for the first time:

override func viewDidLoad() {
    super.viewDidLoad()

    container = try? ModelContainer(for: User.self)
    loadUsers()
}

Tips

Although you can create multiple model containers, most people won't. Instead, create your model container then pass its main context between your views, as needed.

If you want to see it all working, we can add a simple addSamples() method like this one:

@objc func addSamples() {
    let taylor = User(name: "Taylor Swift", birthYear: 1989)
    let beyonce = User(name: "Beyoncé", birthYear: 1981)
    let ed = User(name: "Ed Sheeran", birthYear: 1991)

    container?.mainContext.insert(taylor)
    container?.mainContext.insert(beyonce)
    container?.mainContext.insert(ed)

    loadUsers()
}

Then we can add some more code to viewDidLoad() to configure a navigation bar button. Make sure you embed your view controller in a navigation controller in order to make this toolbar button visible!

Add this to the end of viewDidLoad():

title = "Add Users"

navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Add Samples", style: .plain, target: self, action: #selector(addSamples))

Now you can go ahead and run the app – you'll see that tapping Add Samples correctly updates the list of users.

Again, in UIKit it is your responsibility to make sure your data source remains up to date at all times – the @Query macro is not available to do that task for you.


이찬희 (MarkiiimarK)
Never Stop Learning.