본문 바로가기

프로젝트/ReminderApp clone

04. 저장 기능 구현하기

이제 생성한 List를 CoreData에 저장해야 한다.
MyList의 clolor attribute는 지원하지 않는 타입의 데이터를 저장하기 휘애 Transformable로 설정돼있고, 형변환을 위해 transformer가 필요하다.

//
//  UIColorTransformer.swift
//  ReminderApp
//

import Foundation
import UIKit

class UIColorTransformer: ValueTransformer {
    override func transformedValue(_ value: Any?) -> Any? {
        guard let color = value as? UIColor else { return nil }
        
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: true)
            return data
        } catch {
            return nil
        }
    }
    
    override func reverseTransformedValue(_ value: Any?) -> Any? {
        guard let data = value as? Data else { return nil }
        
        do {
            let color = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data)
            return color
        } catch {
            return nil
        }
    }
}

위에서 지정한 Transformer와 동일한 이름으로 transformer를 정의한다.
해당 클래스는 두가지 메서드를 가지고 있고, transformedValue 메서드는 UIColor를 Data로 변환하는 역할을 하고 있다.
매커니즘은 다음과 같다.

  • 입력되는 데이터가 UIColor 타입인지 확인한다.
  • NSKeyedArchiver를 사용해 UIColor 타입의 데이터를 Data 타입으로 변환한다. 이 때 requireingSecureCoding을 true로 사용해 보안성을 강화한다. 지금은 큰 의미가 없지만 보통은 켜놓는 것이 추천된다.
  • 입력된 데이터가 UIColor가 아니거나 변환에 문제가 있다면 nil을 반환한다.

나머지 reverseTransromedValue 메서드는 코어데이터에서 받아오는 Data를 다시 UIColor로 되돌리는 역할을 한다.
매커니즘은 다음과 같다.

  • 입력되는 데이터가 Data 타입인지 확인한다.
  • NSKeyedUnarchiver를 사용해 Data 타입의 데이터를 UIColor 타입으로 변환한다.
  • 입력된 데이터가 Data타입이 아닌거나 변환에 문제가 있다면 nil을 반환한다.
//
//  CoreDataProvider.swift
//  ReminderApp
//

import Foundation
import CoreData

class CoreDataProvider {
    static let shared = CoreDataProvider()
    let persistentContainer: NSPersistentContainer
    
    private init() {
        
        //register transformers
        ValueTransformer.setValueTransformer(UIColorTransformer(), forName: NSValueTransformerName("UIColorTransformer"))
        
        persistentContainer = NSPersistentContainer(name: "ReminderModel")
        persistentContainer.loadPersistentStores { NSEntityDescription, error in
            if let error {
                fatalError("Error initializing ReminderModel \(error)")
            }
        }
    }
}

정의된 transformer는 CoreDataProvider에 attribute의 내용에 작성한 것과 같은 이름으로 선언해 둔다.
이렇게 해 두면 해당 데이터를 변환할 때 해당 transformer 클래스를 자동으로 사용하게 된다.

//
//  ReminderAppApp.swift
//  ReminderApp
//

import SwiftUI

@main
struct ReminderAppApp: App {
    var body: some Scene {
        WindowGroup {
            HomeView()
                .environment(\.managedObjectContext, CoreDataProvider.shared.persistentContainer.viewContext)
        }
    }
}

이렇게 작성된 CoreDataProvider는 앱 전체에서 공용으로 사용할 수 있도록 environment로 전달한다.

//
//  ReminderService.swift
//  ReminderApp
//

import Foundation
import CoreData
import UIKit

class ReminderService {
    static var viewContext: NSManagedObjectContext {
        CoreDataProvider.shared.persistentContainer.viewContext
    }
    
    static func save() throws {
        try viewContext.save()
    }
    
    static func saveMyList(_ name: String, _ color: UIColor) throws {
        let myList = MyList(context: viewContext)
        myList.name = name
        myList.color = color
        try save()
    }
}

ReminderService 클래스는 본격적인 CoreData와 상호작용하는 메서드를 가지게 된다.
CoreDataProvider 싱글톤 인스턴스이기 때문에 해당 클래스 하나로 중앙 집중식 서비스가 가능하다.
지금은 두 개의 메서드를 작성했고, 하나는 저장의 기능을 담당하는 간단한 save 메서드와 실질적인 List 데이터를 저장하기 위한 saveMyList 메서드이다.

//
//  AddNewListView.swift
//  ReminderApp
//

import SwiftUI

struct AddNewListView: View {
    
    @Environment(\.dismiss) private var dismiss
    @State private var name: String = ""
    @State private var selectedColor: Color = .yellow
    
    let onSave: (String, UIColor) -> Void
    
    private var isFormValid: Bool {
        !name.isEmpty
    }
    
    var body: some View {
        VStack {
            VStack {
                Image(systemName: "line.3.horizontal.circle.fill")
                    .foregroundColor(selectedColor)
                    .font(.system(size: 100))
                
                TextField("List Name", text: $name)
                    .multilineTextAlignment(.center)
                    .textFieldStyle(.roundedBorder)
            }
            .padding(30)
            .clipShape(RoundedRectangle(cornerRadius: 10.0, style: .continuous))
                        
            ColorPickerView(selectedColor: $selectedColor)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
        .toolbar {
            ToolbarItem(placement: .principal) {
                Text("New List")
                    .font(.headline)
            }
            
            ToolbarItem(placement: .topBarLeading) {
                Button("Close") {
                    dismiss()
                }
            }
            
            ToolbarItem(placement: .topBarTrailing) {
                Button("Done") {
                    
                    // TODO: save function
                    onSave(name, UIColor(selectedColor))
                    
                    dismiss()
                }
                .disabled(!isFormValid)
            }
        }

    }
}

해당 saveMyList 메서드는 AddNewListView의 onSave와 연계돼 동작하는데, Done 버튼을 누르면 해당 클로저가 호출되고,

//
//  ContentView.swift
//  ReminderApp
//

import SwiftUI

struct HomeView: View {
    
    @State private var isPresented: Bool = false

    var body: some View {
        NavigationStack {
            VStack {
                Text("Hello World")
                
                Spacer()
                
                HStack {
                    Button {
                        isPresented = true
                    } label: {
                        Text("Add List")
                            .font(.headline)
                    }
                    .padding()
                }
                .frame(maxWidth: .infinity, alignment: .bottomTrailing)
            }
            .sheet(isPresented: $isPresented, content: {
                NavigationStack {
                    AddNewListView { name, color in
                        do {
                            try ReminderService.saveMyList(name, color)
                        } catch {
                            print(error.localizedDescription)
                        }
                    }
                }
            })
        }
        .padding()
    }
}

HomeView에서 해당 closure를 받아 saveMyList 메서드를 호출해 새롭게 작성된 List를 저장한다.