Have a look:
This common data model
import SwiftUI
import UIKit
struct Reminder: Identifiable/*SwiftUI*/, Hashable {
let id: String = UUID().uuidString
let title: String
let dueDate: Date
var notes: String? = nil
var isComplete: Bool = false
let city: String
}
struct City: Identifiable/*SwiftUI*/, Hashable {
var id: String { name }
let name: String
let reminders: [Reminder]
}
class Model: ObservableObject/*SwiftUI*/ {
static let singleton = Model()
let notificationName = Notification.Name("changed")/*UIKit*/
private init() {
debugLoad()
}
var reminders: [Reminder] = [] {
didSet {
cities = calculateCities(reminders: reminders)
}
}
@Published/*SwiftUI*/ var cities: [City] = [] {
didSet {/*UIKit*/
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: nil)
}
}
private func calculateCities(reminders: [Reminder]) -> [City] {
let sortedReminders = reminders.sorted { $0.title < $1.title }
let cities = Dictionary(grouping: sortedReminders, by: \.city)
return cities.sorted { $0.key < $1.key }.map { v in
City(name: v.key, reminders: v.value)
}
}
}
extension Model {
func debugLoad() {
let titles = ["Tom", "Dick", "Harry", "Sally", "Rose", "Alice", "Beth", "Bob", "Pete", "Sarah"]
let cities = ["Loose Bottom", "Plwmp", "Crapstone", "Nether Wallop"]
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [self] timer in
let reminder = Reminder(title: titles.randomElement()!, dueDate: Date(), city: cities.randomElement()!)
withAnimation {/*SwiftUI*/
reminders.append(reminder)
}
if reminders.count > 15 {
timer.invalidate()
}
}
}
}
can drive either (and even both at the same time)
UIKit view implementation
import UIKit
class DataSource: UITableViewDiffableDataSource<City, Reminder> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
sectionIdentifier(for: section)?.name
}
}
class ViewController: UIViewController {
private var dataSource: DataSource!
private var model = Model.singleton
override func viewDidLoad() {
super.viewDidLoad()
let tv = UITableView(frame: view.bounds, style: .grouped)
tv.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tv.register(UITableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: "header")
tv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
dataSource = .init(tableView: tv) { tableView, indexPath, reminder in
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = reminder.title
return cell
}
dataSource.defaultRowAnimation = .fade
tv.dataSource = dataSource
view.addSubview(tv)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
NotificationCenter.default.addObserver(forName: model.notificationName, object: nil, queue: .main) { [weak self] _ in
guard let self = self else { return }
var snapshot = NSDiffableDataSourceSnapshot<City, Reminder>()
snapshot.appendSections(self.model.cities)
for city in self.model.cities {
snapshot.appendItems(city.reminders, toSection: city)
}
self.dataSource.apply(snapshot, animatingDifferences: true)
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self, name: model.notificationName, object: nil)
}
}
and
SwiftUI view implementation
import SwiftUI
struct ContentView: View {
@ObservedObject private var model = Model.singleton
var body: some View {
List {
ForEach(model.cities) { city in
Section(city.name) {
ForEach(city.reminders) { reminder in
Text(reminder.title)
}
}
}
}
}
}
A few notes:
- for simplicity of this example I do everything on the main thread. In real code model calculations could be done on background queue, model publishing has to be done on the main thread for SwiftUI and on either main thread or background queue (but don't mix) for UIKit's diffable data source implementation. Adjust accordingly.
- I labeled the corresponding features with
/*SwiftUI*/
or/*UIKit*/
to show which one is needed for which implementation. - SwiftUI code is more clean, concise and easier to follow. It's about 3x shorter, I found the similar reduction in big real apps.
- If it feels like "double work" in case of UIKit (first we create a model data structure, then we replicate it in the form of "snapshot") - this is indeed a duplication... UIKit's diffable implementation gears you towards their view of the world where their "snapshot" is the source of truth and they want you to use special methods to modify snapshot. You can still recreate the whole snapshot and it would not cause excessive view update (e.g. if only a single item changed - that only item would be redrawn).
- Note that
Identifiable
is not required nor used in UIKit's diffable implementation... Besides other things it means it can not possibly do a proper animation when changing from:- "1.Sun" ... other items... "8.Moon" to
- "8.Moon" ... other items ... "1.Sun"
in this case the first and last items would be just redrawn whilst the wanted behaviour (supported by SwiftUI which usesIdentifiable
) would do a proper move / exchange animation for these two items. If it is a must to do that animation properly you'll have to use the convoluted and error prone manual implementation that isn't based on diffable datasource.