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 |