Section 12/181 menit
12. Integrasi dengan UIKit
12. Integrasi dengan UIKit
SwiftData tidak memiliki equivalent @Query untuk UIKit — developer perlu melakukan fetch manual dan update UI secara eksplisit.
ViewModel Pattern dengan ModelContext
swift
// MARK: - ViewModel
@MainActor
final class BookListViewModel {
private let modelContext: ModelContext
private(set) var books: [Book] = []
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
func loadBooks(filter: String = "") throws {
var descriptor = FetchDescriptor<Book>(
sortBy: [SortDescriptor(\Book.title)]
)
if !filter.isEmpty {
descriptor.predicate = #Predicate {
$0.title.localizedStandardContains(filter) ||
$0.author.localizedStandardContains(filter)
}
}
books = try modelContext.fetch(descriptor)
}
func addBook(title: String, author: String, year: Int) throws {
let book = Book(title: title, author: author, publishedYear: year)
modelContext.insert(book)
try modelContext.save()
try loadBooks()
}
func deleteBook(at indexPath: IndexPath) throws {
let book = books[indexPath.row]
modelContext.delete(book)
try modelContext.save()
books.remove(at: indexPath.row)
}
func toggleRead(_ book: Book) throws {
book.isRead.toggle()
try modelContext.save()
}
}
// MARK: - UIViewController
final class BookListViewController: UIViewController {
private var viewModel: BookListViewModel!
private lazy var tableView = UITableView(frame: .zero, style: .insetGrouped)
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
setupNavigationBar()
// Inject ViewModel (dari AppDelegate atau Coordinator)
let context = (UIApplication.shared.delegate as! AppDelegate).modelContainer.mainContext
viewModel = BookListViewModel(modelContext: context)
loadData()
}
private func loadData() {
try? viewModel.loadBooks()
tableView.reloadData()
}
@objc private func addBookTapped() {
let alert = UIAlertController(title: "Tambah Buku", message: nil, preferredStyle: .alert)
alert.addTextField { $0.placeholder = "Judul" }
alert.addTextField { $0.placeholder = "Penulis" }
alert.addAction(UIAlertAction(title: "Simpan", style: .default) { [weak self] _ in
guard let title = alert.textFields?[0].text, !title.isEmpty,
let author = alert.textFields?[1].text, !author.isEmpty else { return }
try? self?.viewModel.addBook(title: title, author: author, year: 2024)
self?.tableView.reloadData()
})
alert.addAction(UIAlertAction(title: "Batal", style: .cancel))
present(alert, animated: true)
}
private func setupNavigationBar() {
title = "Koleksi Buku"
navigationItem.rightBarButtonItem = UIBarButtonItem(
systemItem: .add,
primaryAction: UIAction { [weak self] _ in self?.addBookTapped() }
)
}
private func setupTableView() {
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
}
extension BookListViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel.books.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let book = viewModel.books[indexPath.row]
var config = cell.defaultContentConfiguration()
config.text = book.title
config.secondaryText = "\(book.author) (\(book.publishedYear))"
config.image = UIImage(systemName: book.isRead ? "checkmark.circle.fill" : "circle")
config.imageProperties.tintColor = book.isRead ? .systemGreen : .systemGray
cell.contentConfiguration = config
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle,
forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
try? viewModel.deleteBook(at: indexPath)
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let book = viewModel.books[indexPath.row]
try? viewModel.toggleRead(book)
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
// MARK: - AppDelegate Setup
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var modelContainer: ModelContainer!
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
do {
modelContainer = try ModelContainer(for: Book.self, Author.self)
} catch {
fatalError("SwiftData setup failed: \(error)")
}
return true
}
}