iOS - SwiftData CRUD

이한솔·2024년 1월 23일
0

iOS 앱개발 🍏

목록 보기
40/44

SwiftData CRUD

SwiftData를 공부하면서 아직 SwiftUI는 다뤄본 적이 없는데 대부분의 블로그 글들이 SwiftUI로 작성되어있었다. SwiftData가 SwiftUI와 자연스럽게 통합되기 때문인 것 같은데 아직 어떤 부분이 자연스럽게 통합된다는건지 구체적으로 이해가 되지는 않는다!

우선 공부중인 UIKit를 기반으로 SwiftData를 사용해봤다.
이름과 나이를 저장하는 테스트 프로젝트를 만들었다.



SwiftData 구현

  1. Person Model을 만든다.
import Foundation
import SwiftData

@Model
class Person {
    var name: String
    var age: String
    
    init(name: String, age: String) {
        self.name = name
        self.age = age
    }
    
}
  1. MainView와 ListView를 만들어줬다.
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)
        ])
    }
    
}
  1. SwiftDataManager 파일을 따로 만들어 로직을 분리해주었다.
    SwiftData를 사용해보는 것이 목표였기 때문에 쉽게 접근할 수 있도록 싱글턴 패턴으로 만들어줬다.
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)")
        }
    }

    
}
  1. Controller 파일에서 addButton이나 테이블뷰셀 delete시 SwiftDataManager의 로직이 실행된다.
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)
    }
    
}


CoreData와 SwiftData

CoreData와 SwiftData의 차이점

프레임워크 vs 라이브러리
CoreData: 애플이 제공하는 프레임워크
SwiftData: 오픈소스 라이브러리

데이터 모델 정의 방식
CoreData: Core Data 모델 편집기에서 사용하는 .xcdatamodeld 파일을 사용한다.
SwiftData: Swift 클래스를 사용하고 @Model 어트리뷰트를 사용하여 데이터 모델을 선언한다.

CoreData와 SwiftData의 공통점

CoreData와 SwiftData 모두 데이터베이스 관리를 위한 라이브러리라는 공통점이 있다. 또한 CoreData와 SwiftData 두개 다 SQL 쿼리를 직접 작성하지 않고도 데이터베이스 작업을 수행할 수 있고, local storage에 데이터가 저장되어 앱을 종료했다 재실행해도 데이터가 남아있지만 앱을 삭제하면 데이터가 삭제된다.

현재는 SwiftData를 iOS 17 이전 버전에서 사용이 불가능해서 앱에서 낮은 iOS 버전을 지원하려면 Core Data를 사용해야한다!!

1개의 댓글

comment-user-thumbnail
2024년 1월 26일

열심히 하시네요 !! 잘 보고 갑니다 ~~

답글 달기