SwiftData를 공부하면서 아직 SwiftUI는 다뤄본 적이 없는데 대부분의 블로그 글들이 SwiftUI로 작성되어있었다. SwiftData가 SwiftUI와 자연스럽게 통합되기 때문인 것 같은데 아직 어떤 부분이 자연스럽게 통합된다는건지 구체적으로 이해가 되지는 않는다!
우선 공부중인 UIKit를 기반으로 SwiftData를 사용해봤다.
이름과 나이를 저장하는 테스트 프로젝트를 만들었다.
import Foundation
import SwiftData
@Model
class Person {
var name: String
var age: String
init(name: String, age: String) {
self.name = name
self.age = age
}
}
class MainView: UIView {
private let nameLabel: UILabel = {
let label = UILabel()
label.text = "이름"
return label
}()
let nameTextField: UITextField = {
let tf = UITextField()
tf.layer.borderWidth = 1.0
tf.layer.cornerRadius = 10.0
return tf
}()
private let ageLabel: UILabel = {
let label = UILabel()
label.text = "나이"
return label
}()
let ageTextField: UITextField = {
let tf = UITextField()
tf.layer.borderWidth = 1.0
tf.layer.cornerRadius = 10.0
return tf
}()
let addButton: UIButton = {
let btn = UIButton()
btn.setTitle(" 추가하기 ", for: .normal)
btn.backgroundColor = .lightGray
btn.setTitleColor(.black, for: .normal)
return btn
}()
let showButton: UIButton = {
let btn = UIButton()
btn.setTitle(" 데이터 ", for: .normal)
btn.backgroundColor = .lightGray
btn.setTitleColor(.black, for: .normal)
return btn
}()
override init(frame: CGRect) {
super.init(frame: frame)
setUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setUI() {
self.backgroundColor = .systemBackground
addSubview(nameLabel)
addSubview(nameTextField)
addSubview(ageLabel)
addSubview(ageTextField)
addSubview(addButton)
addSubview(showButton)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameTextField.translatesAutoresizingMaskIntoConstraints = false
ageLabel.translatesAutoresizingMaskIntoConstraints = false
ageTextField.translatesAutoresizingMaskIntoConstraints = false
addButton.translatesAutoresizingMaskIntoConstraints = false
showButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nameLabel.topAnchor.constraint(equalTo: topAnchor, constant: 250),
nameLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30),
nameLabel.widthAnchor.constraint(equalToConstant: 40),
nameTextField.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor),
nameTextField.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 30),
nameTextField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -30),
nameTextField.heightAnchor.constraint(equalToConstant: 40),
ageLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 40),
ageLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30),
ageLabel.widthAnchor.constraint(equalToConstant: 40),
ageTextField.centerYAnchor.constraint(equalTo: ageLabel.centerYAnchor),
ageTextField.leadingAnchor.constraint(equalTo: ageLabel.trailingAnchor, constant: 30),
ageTextField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -30),
ageTextField.heightAnchor.constraint(equalToConstant: 40),
addButton.topAnchor.constraint(equalTo: ageTextField.bottomAnchor, constant: 100),
addButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 120),
showButton.topAnchor.constraint(equalTo: ageTextField.bottomAnchor, constant: 100),
showButton.leadingAnchor.constraint(equalTo: addButton.trailingAnchor, constant: 10)
])
}
}
class ListView: UIView {
let tableView: UITableView = {
let tv = UITableView()
tv.backgroundColor = .white
tv.layer.borderWidth = 1.0
tv.layer.cornerRadius = 10
tv.layer.masksToBounds = true
tv.layer.borderColor = UIColor.black.cgColor
return tv
}()
override init(frame: CGRect) {
super.init(frame: frame)
setTableView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setTableView() {
self.backgroundColor = .systemBackground
addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10),
tableView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 30),
tableView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -30),
tableView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -10)
])
}
}
class SwiftDataManager {
static var shared = SwiftDataManager()
var container = try? ModelContainer(for: Person.self)
// Create
@MainActor func createPerson(name: String, age: String) {
let person = Person(name: name, age: age)
do {
container?.mainContext.insert(person)
try container?.mainContext.save()
} catch {
print("생성 실패: \(error.localizedDescription)")
}
}
// Read
@MainActor func getPerson() -> [Person] {
// FetchDescriptor를 사용하여 Person 엔터티의 데이터를 가져옴
let descriptor = FetchDescriptor<Person>()
// container?.mainContext.fetch(descriptor)를 통해 데이터베이스에서 메모를 가져옴
let persons = (try? container?.mainContext.fetch(descriptor)) ?? []
return persons
}
// Update
@MainActor func updatePerson(name: String, age: String, index: Int) {
let descriptor = FetchDescriptor<Person>()
do {
guard let result = try container?.mainContext.fetch(descriptor) else {
return
}
if index < result.count {
let personObject = result[index]
personObject.name = name
personObject.age = age
try container?.mainContext.save()
}
} catch {
print("수정실패: \(error.localizedDescription)")
}
}
// Delete
@MainActor func deletePerson(person: Person) {
container?.mainContext.delete(person)
do {
try container?.mainContext.save()
} catch {
print("삭제 실패: \(error.localizedDescription)")
}
}
}
class MainViewController: UIViewController {
private let mainView = MainView()
override func loadView() {
view = mainView
}
override func viewDidLoad() {
super.viewDidLoad()
setAddTarget()
}
private func setAddTarget() {
mainView.addButton.addTarget(self, action: #selector(addButtonTapped), for: .touchUpInside)
mainView.showButton.addTarget(self, action: #selector(showButtonTapped), for: .touchUpInside)
}
@objc private func addButtonTapped() {
guard let name = mainView.nameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines),
!name.isEmpty,
let age = mainView.ageTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines),
!age.isEmpty else {
print("이름이나 나이를 입력하세요")
return
}
SwiftDataManager.shared.createPerson(name: name, age: age)
setTextField()
}
@objc private func showButtonTapped() {
let listVC = ListViewController()
navigationController?.pushViewController(listVC, animated: true)
}
private func setTextField() {
mainView.nameTextField.text = ""
mainView.ageTextField.text = ""
}
}
class ListViewController: UIViewController {
private let listView = ListView()
private var personList: [Person]!
override func loadView() {
view = listView
}
override func viewDidLoad() {
super.viewDidLoad()
setTabelView()
setPersonList()
}
private func setTabelView() {
listView.tableView.dataSource = self
listView.tableView.register(TableViewCell.self, forCellReuseIdentifier: "cell")
}
private func setPersonList() {
personList = SwiftDataManager.shared.getPerson()
}
}
// MARK: - UITableViewDataSource
extension ListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if let person = personList?[indexPath.row] {
SwiftDataManager.shared.deletePerson(person: person)
setPersonList()
}
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return personList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? TableViewCell else {
return UITableViewCell()
}
cell.nameLabel.text = personList[indexPath.row].name
cell.ageLabel.text = personList[indexPath.row].age
cell.editButtonClosure = { [weak self] in
guard let self = self else { return }
self.showAlert(index: indexPath.row)
}
return cell
}
private func showAlert(index: Int) {
let alertController = UIAlertController(title: "데이터 수정", message: nil, preferredStyle: .alert)
alertController.addTextField { textField in
textField.placeholder = "이름"
}
alertController.addTextField { textField in
textField.placeholder = "나이"
}
let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil)
let saveAction = UIAlertAction(title: "확인", style: .default) { _ in
guard let newName = alertController.textFields?[0].text,
let newAge = alertController.textFields?[1].text,
!newName.trimmingCharacters(in: .whitespaces).isEmpty,
!newAge.trimmingCharacters(in: .whitespaces).isEmpty else {
alertController.textFields?[0].placeholder = "이름을 입력해 주세요."
alertController.textFields?[1].placeholder = "나이를 입력해 주세요."
return
}
SwiftDataManager.shared.updatePerson(name: newName, age: newAge, index: index)
self.listView.tableView.reloadData()
}
alertController.addAction(cancelAction)
alertController.addAction(saveAction)
self.present(alertController, animated: true, completion: nil)
}
}
프레임워크 vs 라이브러리
CoreData: 애플이 제공하는 프레임워크
SwiftData: 오픈소스 라이브러리
데이터 모델 정의 방식
CoreData: Core Data 모델 편집기에서 사용하는 .xcdatamodeld 파일을 사용한다.
SwiftData: Swift 클래스를 사용하고 @Model 어트리뷰트를 사용하여 데이터 모델을 선언한다.
CoreData와 SwiftData 모두 데이터베이스 관리를 위한 라이브러리라는 공통점이 있다. 또한 CoreData와 SwiftData 두개 다 SQL 쿼리를 직접 작성하지 않고도 데이터베이스 작업을 수행할 수 있고, local storage에 데이터가 저장되어 앱을 종료했다 재실행해도 데이터가 남아있지만 앱을 삭제하면 데이터가 삭제된다.
현재는 SwiftData를 iOS 17 이전 버전에서 사용이 불가능해서 앱에서 낮은 iOS 버전을 지원하려면 Core Data를 사용해야한다!!
열심히 하시네요 !! 잘 보고 갑니다 ~~