본문 바로가기

학습 노트/Swift UI (2022)

31. CoreData #1

CoreData #1


SwiftUI에서 CoreData를 사용해 CRUD를 구현해 본다.

CoreData에는 MemberEntity가 존재하고, name과 age를 저장한다.

SwiftUI에서 CoreData에 접근하는 방식은
UIKit에서 싱글톤 객체를 생성한 다음 mainContext에 접근하는 방식이 아닌,
CoreData 자체를 공유 데이터로 설정해 각 View에서 접근하는 방식을 주로 사용한다.

CoreData를 공유 데이터로 설정하기.

@main
struct DataPersistenceApp: App {
    let manager = CoreDataManager.shared

    var body: some Scene {
        WindowGroup {
            MainList()
                .environment(\.managedObjectContext, manager.mainContext)
        }
    }
}

@main에서 CoreData를 environment로 전달한다.

struct MemberList: View {
    let members = [MemberEntity]()

    @Environment(\.managedObjectContext) var context

    @State private var showComposer = false
    @State private var editTarget: MemberEntity?

    @State private var selectedSortType = SortType.types[0].id
    @State private var keyword = ""
    .
    .
    .

main에서 선언한 environment를 사용할 View에 적용하고

struct MemberList_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            MemberList()
                .navigationTitle("Members")
                .environment(\.managedObjectContext, CoreDataManager.shared.mainContext)
        }
    }
}

preview에도 전달한다.
CoreData에 접근하는 View들은 이런 방식으로 사전 작업이 필요하다.

Create

struct MemberCompose: View {
    let editTarget: MemberEntity?

    @State private var name: String = ""
    @State private var age: Int? = nil

    @Environment(\.managedObjectContext) var context
    @Environment(\.dismiss) var dismiss

    .
    .
    .
struct MemberCompose_Previews: PreviewProvider {
    static var previews: some View {
        MemberCompose(editTarget: nil)
            .environment(\.managedObjectContext, CoreDataManager.shared.mainContext)
    }
}

Create를 구현할 View에서도 동일한 방식으로 context를 추가한다.

func addMember() {
    let newMember = MemberEntity(context: context)

    newMember.name = name
    newMember.age = Int16(age ?? 0)

    do {
        try context.save()
    } catch {
        print(error)
    }

    dismiss()
}

추가한 context를 사용해서 새 MemberEntity를 생성하고,
필요한 속성들을 저장한다.
이후 해당 context를 저장한 후, 화면을 닫아주면 된다.

Read

UIKit과 SwiftUI의 방식이 확연히 다르다.

struct MemberList: View {
    @FetchRequest(sortDescriptors: [])
    var members: FetchedResults<MemberEntity>

    @Environment(\.managedObjectContext) var context

    @State private var showComposer = false
    @State private var editTarget: MemberEntity?

    @State private var selectedSortType = SortType.types[0].id
    @State private var keyword = ""
    .
    .
    .

'@FetchRequest'를 사용해 Fetch를 진행하고, 이 결과를 변수에 저장해 사용하면 된다.
이 경우 변수를 사용하는 View와 쌍방향으로 연결되기 때문에 별도의 새로고침 없이,
CoreData의 context가 업데이트되는 등 변화가 생기면 자동으로 반영된다.

여기까지 진행되면 CoreData에 새로운 데이터를 저장하고,
이를 읽어 올 수 있게 된다.

Update

struct MemberList: View {
    @FetchRequest(sortDescriptors: [])
    var members: FetchedResults<MemberEntity>

    @Environment(\.managedObjectContext) var context

    @State private var showComposer = false
    @State private var editTarget: MemberEntity?

    @State private var selectedSortType = SortType.types[0].id
    @State private var keyword = ""

    var body: some View {
        List {
            ForEach(members) { member in
                Button {
                    editTarget = member
                } label: {
                    HStack {
                        Text(member.name!)
                            .foregroundColor(.primary)
                        Spacer()
                        Text("\(member.age)")
                            .foregroundColor(.secondary)
                    }
                }
            }
            .onDelete(perform: delete(at:))
        }
        .sheet(item: $editTarget, content: { target in
            MemberCompose(editTarget: target)
        })
        .sheet(isPresented: $showComposer, content: {
            MemberCompose(editTarget: nil)                
        })

업데이트는 별도의 화면을 사용하지 않고, Compose View의 화면을 그대로 사용한다.

@State private var editTarget: MemberEntity?

Compse View로 전달할 MemberEntity 형식의 변수를 하나 생성한다.

ForEach(members) { member in
    Button {
        editTarget = member
    } label: {
        HStack {
            Text(member.name!)
                .foregroundColor(.primary)
            Spacer()
            Text("\(member.age)")
                .foregroundColor(.secondary)
        }
    }
}

Cell을 터치하면 해당 변수에 데이터를 저장하고

.sheet(item: $editTarget, content: { target in
    MemberCompose(editTarget: target)
})
.sheet(isPresented: $showComposer, content: {
    MemberCompose(editTarget: nil)                
})

이를 Compse View에 전달한다.

struct MemberCompose: View {
    let editTarget: MemberEntity?
    
    @State private var name: String = ""
    @State private var age: Int? = nil
    
	@Environment(\.managedObjectContext) var context
    @Environment(\.dismiss) var dismiss

Compose View에서는 editTarget을 전달받아 처리하면 된다.

.toolbar {
    ToolbarItemGroup(placement: .navigationBarLeading) {
        Button {
            dismiss()
        } label: {
            Text("Cancel")
        }
    }

    ToolbarItemGroup(placement: .navigationBarTrailing) {
        Button {
            if let _ = editTarget {
                editMember()
            } else {
                addMember()
            }

            dismiss()
        } label: {
            Text("Save")
        }
    }
}
.navigationTitle(editTarget != nil ? "멤버 수정" : "새 멤버")

editTarget의 존재 유무에 따라 navigationTitle을 변경하고,
저장 버튼의 동작도 구분해 실행한다.

func editMember() {
    guard let editTarget = editTarget else {
        return
    }

    editTarget.name = name
    editTarget.age = Int16(age ?? 0)

    do {
        try context.save()
    } catch {
        print(error)
    }

    dismiss()
}

수정 시 호출하는 editMember 함수의 구조는 동일하다.
editTarget이 제대로 전달됐는지 검증한 후 해당 Context를 수정해 저장하고 Sheet를 닫으면 종료된다.

Form {
    TextField("Name", text: $name)
        .onAppear{
            if let editTarget = editTarget {
                name = editTarget.name!
            }
        }

    TextField("Age", value: $age, format: .number)
        .onAppear{
            if let editTarget = editTarget {
                age = Int(editTarget.age)
            }
        }
        .keyboardType(.numberPad)

}

editTarget이 전달된 경우 전달된 데이터를 활용하는 것도 방법이다.
전달된 데이터를 TextField에 뿌려 어떤 값을 수정하는지 사용자에게 보여주도록 구현했다.

Delete

var body: some View {
    List {
        ForEach(members) { member in
            Button {
                editTarget = member
            } label: {
                HStack {
                    Text(member.name!)
                        .foregroundColor(.primary)
                    Spacer()
                    Text("\(member.age)")
                        .foregroundColor(.secondary)
                }
            }
        }
        .onDelete(perform: delete(at:))
    }

삭제 기능은 List의 onDelete modifier를 사용한다.

func delete(at rows: IndexSet) {
    rows.forEach { index in
        context.delete(members[index])
    }

    do {
        try context.save()
    } catch {
        print(error)
    }
}

호출되는 delete 메서드는
해당 Cell의 위치를 받아 context에서 삭제한다.
이후 context를 저장하면 완료된다.

'학습 노트 > Swift UI (2022)' 카테고리의 다른 글

33. AppStorage & SceneStorage  (0) 2022.11.17
32. CoreData #2  (0) 2022.11.17
30. Gesture  (0) 2022.11.16
29. List의 부가 기능 구현하기  (0) 2022.11.10
28. ForEach & Grid  (0) 2022.11.09