apple/Docs, iOS, Swift

[iOS] CoreData 정리 2 실습 (2/2)

lgvv 2023. 12. 10. 22:19

[iOS] CoreData 정리 2 (실습) (2/2)

 

1편은 이론, 2편에서는 코드를 작성하면서 알아봅시다.

 

1편: https://rldd.tistory.com/586

 

 

CoreDataSampleCode

 

 

Core Data를 사용하기 위한 사전준비

- 프로젝트 생성시

Storage를 Core Data를 설정

 

이렇게 생성하면 AppDelegate와 SceneDelegate에 기본적으로 Core Data를 위한 코드가 추가됩니다.

 

(좌) AppDelegate.swift (우) SceneDelegate.swift

 

먼저 SceneDelegate에서는 백그라운드로 넘어갔을때 현재 컨텍스트를 저장하고 있습니다.

해당 컨텍스트의 hasChanges 즉 변화가 있다면 save를 하고 있습니다.

 

먼저 persistentContainer 변수를 살펴봅시다.

NSPersistentContainer(name: ... )에 name안에는 내가 생성한 모델의 이름을 넣어줍니다.

해당 모델은 아래 사진의 파일 이름일치해야합니다.

실습에 사용한 모델.

 

Model파일에서 Memo 엔티티 중 title Attibute를 확인해봅시다.

우측 인스펙터를 확인해보면 Type 등을 설정할 수 있습니다.

 

 

실습 UI

 

이번 실습에 사용할 UI

 

좌측 첫번째 사진에서는 coreData를 가장 기본적으로 사용하는 방법에 대해서 알아봅니다.

우측 두번째 사진에서는 coreData 구조를 개선하고, SwiftUI에서도 사용할 수 있는 형태에 대해서 알아봅니다.

 

 

해당 형태로 CRUD 과정을 수행할 수 있습니다.

FirstViewController

import UIKit
import SwiftUI
import SnapKit
import RxSwift
import RxCocoa
import CoreData

final class FirstViewController: UIViewController {
    var persistentContainer: NSPersistentContainer? {
        (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer
    }
    
    private var diposeBag = DisposeBag()
    
    private func bind() { 
        createButton.rx.tap
            .debug("createButton Tapped")
            .bind(with: self) { this, _ in
                this.create()
            }.disposed(by: diposeBag)
        
        readButton.rx.tap
            .debug("readButton Tapped")
            .bind(with: self) { this, _ in
                this.read()
            }.disposed(by: diposeBag)
        
        updateButton.rx.tap
            .debug("updateButton Tapped")
            .bind(with: self) { this, _ in
                this.update()
            }.disposed(by: diposeBag)
        
        deleteButton.rx.tap
            .debug("deleteButton Tapped")
            .bind(with: self) { this, _ in
                this.delete()
            }.disposed(by: diposeBag)
        
        routeButton.rx.tap
            .bind(with: self) { this, _ in
                let vc = SecondViewController()
                vc.memoDiskCache = MemoDiskCache.defaultValue
                this.navigationController?.pushViewController(vc, animated: true)
            }.disposed(by: diposeBag)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureLayout()
        bind()
    }
    
    private func create() {
        guard let context = persistentContainer?.viewContext else { return }
        
        let memo = Memo(context: context)
        memo.title = "제목"
        memo.contents = "임시내용"
        
        let date = Date()
        memo.createAt = date
        memo.updateAt = date
        
        try? context.save()
    }
    
    private func read() {
        guard let context = self.persistentContainer?.viewContext else { return }
        
        let request = Memo.fetchRequest()
        guard let memos = try? context.fetch(request) else { return }
        
        print(memos)
    }
    
    private func update() {
        guard let context = self.persistentContainer?.viewContext else { return }
        
        let request = Memo.fetchRequest()
        guard let memos = try? context.fetch(request) else { return }
        
        let filteredMemos = memos.filter { $0.title == "제목" }
        
        filteredMemos.forEach {
            $0.title = "CARROT!"
            $0.updateAt = Date()
        }
        
        try? context.save()
    }
    
    private func delete() {
        guard let context = self.persistentContainer?.viewContext else { return }
        
        let request = Memo.fetchRequest()
        guard let memos = try? context.fetch(request) else { return }
        memos.forEach { context.delete($0) }
        let filteredMemos = memos
            .filter { $0.title == "제목" }
        
        filteredMemos.forEach { context.delete($0) }
        
        try? context.save()
    }
    
    // MARK: - UIComponents
    
    private lazy var titleLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 20, weight: .bold)
        label.text = String(describing: Self.self)
        return label
    }()
    
    private lazy var container: UIStackView = {
        let stackView = UIStackView()
        stackView.distribution = .fillProportionally
        stackView.spacing = 3
        return stackView
    }()
    
    private lazy var createButton: UIButton = {
        let button = makeButton("create")
        return button
    }()
    
    private lazy var readButton: UIButton = {
        let button = makeButton("read")
        return button
    }()

    private lazy var updateButton: UIButton = {
        let button = makeButton("update")
        return button
    }()
    
    private lazy var deleteButton: UIButton = {
        let button = makeButton("delete")
        return button
    }()
    
    private lazy var routeButton: UIButton = {
        let button = makeButton("go to second viewController")
        return button
    }()
    
    private func makeButton(_ title: String) -> UIButton {
        let button = UIButton()
        button.setTitle(title, for: .normal)
        button.backgroundColor = .blue
        button.layer.cornerRadius = 6
        return button
    }
    
    // MARK: - Layout
    
    private func configureLayout() {
        view.addSubview(titleLabel)
        titleLabel.snp.makeConstraints {
            $0.bottom.equalToSuperview().dividedBy(4)
            $0.centerX.equalToSuperview()
        }
        
        view.addSubview(container)
        container.snp.makeConstraints {
            $0.leading.trailing.equalToSuperview()
            $0.center.equalToSuperview()
        }
        container.addArrangedSubview(createButton)
        container.addArrangedSubview(readButton)
        container.addArrangedSubview(updateButton)
        container.addArrangedSubview(deleteButton)
        
        view.addSubview(routeButton)
        routeButton.snp.makeConstraints {
            $0.top.equalTo(container.snp.bottom).offset(50)
            $0.centerX.equalToSuperview()
        }
    }
}

 

SecondViewController

이번에는 EnvironmentKey, EnvironmentValue를 통해 @Environment형태로 사용 가능하도록 구현해 보았습니다.

TCA로 치면, @Dependency 닮아 있습니다.

 

import UIKit
import SwiftUI
import SnapKit
import RxSwift
import RxCocoa
import CoreData

final class SecondViewController: UIViewController {
    private var diposeBag = DisposeBag()
    
    var memoDiskCache: MemoDiskCache?
    
    private func bind() {
        createButton.rx.tap
            .debug("createButton Tapped")
            .bind(with: self) { this, _ in
                this.memoDiskCache?.create("🎄 크리스마스", "")
            }.disposed(by: diposeBag)
        
        readButton.rx.tap
            .debug("readButton Tapped")
            .bind(with: self) { this, _ in
                let memos = this.memoDiskCache?.read()
                memos?.forEach {
                    print("\($0.title)")
                }
            }.disposed(by: diposeBag)
        
        updateButton.rx.tap
            .debug("updateButton Tapped")
            .bind(with: self) { this, _ in
                this.memoDiskCache?.update("🎄 크리스마스")
            }.disposed(by: diposeBag)
        
        deleteButton.rx.tap
            .debug("deleteButton Tapped")
            .bind(with: self) { this, _ in
                this.memoDiskCache?.delete()
            }.disposed(by: diposeBag)
    }
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        
        configureLayout()
        bind()
    }
    
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - UIComponents
    
    private lazy var titleLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 20, weight: .bold)
        label.text = String(describing: Self.self)
        return label
    }()
    
    private lazy var countLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 30, weight: .bold)
        return label
    }()
    
    private lazy var container: UIStackView = {
        let stackView = UIStackView()
        stackView.distribution = .fillProportionally
        stackView.spacing = 3
        return stackView
    }()
    
    private lazy var createButton: UIButton = {
        let button = makeButton("create")
        return button
    }()
    
    private lazy var readButton: UIButton = {
        let button = makeButton("read")
        return button
    }()

    private lazy var updateButton: UIButton = {
        let button = makeButton("update")
        return button
    }()
    
    private lazy var deleteButton: UIButton = {
        let button = makeButton("delete")
        return button
    }()
    
    private func makeButton(_ title: String) -> UIButton {
        let button = UIButton()
        button.setTitle(title, for: .normal)
        button.backgroundColor = .blue
        button.layer.cornerRadius = 6
        return button
    }
    
    // MARK: - Layout
    
    private func configureLayout() {
        view.backgroundColor = .systemBackground
        
        view.addSubview(titleLabel)
        titleLabel.snp.makeConstraints {
            $0.bottom.equalToSuperview().dividedBy(4)
            $0.centerX.equalToSuperview()
        }
        
        view.addSubview(countLabel)
        countLabel.snp.makeConstraints {
            $0.top.equalTo(titleLabel.snp.bottom).offset(10)
            $0.centerX.equalToSuperview()
        }
        
        view.addSubview(container)
        container.snp.makeConstraints {
            $0.leading.trailing.equalToSuperview()
            $0.center.equalToSuperview()
        }
        container.addArrangedSubview(createButton)
        container.addArrangedSubview(readButton)
        container.addArrangedSubview(updateButton)
        container.addArrangedSubview(deleteButton)
    }
}

import Combine

struct MemoDiskCache: EnvironmentKey {
    
    var create: (_ title: String, _ contents: String) -> ()
    var read: () -> [Memo]
    var update: (_ title: String) -> ()
    var delete: () -> ()
    
    enum MemoDiskCacheError: Error {
        case create
        case read
        case update
        case delete
    }
    
    static let defaultValue: MemoDiskCache = .init { (title, contents) in
        var persistentContainer: NSPersistentContainer? {
            (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer
        }
        guard let context = persistentContainer?.viewContext else { return }
        
        var memoContext = Memo(context: context)
        memoContext.title = title
        memoContext.contents = contents
        
        let date = Date()
        memoContext.createAt = date
        memoContext.updateAt = date
        
        do {
            try context.save()
        } catch {
            
        }
    } read: {
        var persistentContainer: NSPersistentContainer? {
            (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer
        }
        guard let context = persistentContainer?.viewContext else { return [] }
        
        let request = Memo.fetchRequest()
        guard let memos = try? context.fetch(request) else { return [] }
        
        return memos
    } update: { title in
        var persistentContainer: NSPersistentContainer? {
            (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer
        }
        
        guard let context = persistentContainer?.viewContext else { return }
        let request = Memo.fetchRequest()
        guard let memos = try? context.fetch(request) else { return }
        
        do {
            let filteredMemos = memos.filter { $0.title == title }
            
            filteredMemos.forEach {
                $0.title = "🎁 산타의 선물"
                $0.updateAt = Date()
            }
            
            try context.save()
        } catch {
            
        }
    } delete: {
        var persistentContainer: NSPersistentContainer? {
            (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer
        }
        guard let context = persistentContainer?.viewContext else { return }
        
        let request = Memo.fetchRequest()
        guard let memos = try? context.fetch(request) else { return }
        
        let filteredMemos = memos
            .filter { $0.title == "🎁 산타의 선물" }
        
        filteredMemos.forEach { context.delete($0) }
        
        try? context.save()
    }

}

extension EnvironmentValues {
    var memoDiskCache: MemoDiskCache {
        get { self[MemoDiskCache.self] }
        set { self[MemoDiskCache.self] = newValue }
    }
}

 

 

액션에 따른 흐름도.

데이터 저장

 

'apple > Docs, iOS, Swift' 카테고리의 다른 글

[Natural Language] Overview  (0) 2024.03.27
[iOS] SwiftData in UIKit  (0) 2023.12.15
[iOS] CoreData 정리 이론 (1/2)  (0) 2023.12.10
[Swift] plain ol' data(POD)  (0) 2023.08.08
[UIKit] UILabel Inset  (0) 2023.06.23