그동안 궁금했던 점 : 앱을 껐다 켜도 내가 데이터를 추가, 수정, 삭제한 내역이 저장되게 하려면 어떻게 해야 할까?
Core Data를 이용하면 이게 가능했다. 간단한 예시를 통해 Core Data의 핵심을 살펴보자
- Data Model 파일 생성
- Add Entity & Add Attribute
- Data Controller 파일 생성 ( Core Data를 불러오고 재가공하는 함수 코드가 담긴 파일. 일종의 ViewModel 역할을 하는듯? )
- App 파일에서 Data를 이 플젝 전역에 뿌려주기
- View 파일 생성 & 코딩
1. Data Model 파일 생성
처음에 Use Core Data 체크 안해도 new file을 통해 Data Model 파일을 생성할 수 있다. 생성한 후,
2. Add Entity & Add Attribute
Add Entity를 통해 Food라는 데이터 형식? 틀? 생성하고
Attribute를 추가해주면서 Food안에 들어갈 id, date, name, calories을 생성하고, 데이터 타입을 선언해준다.
3. Data Controller 파일 생성
이 파일은 Core Data를 View에 던져주기 위해 가공하고, Core Data의 CRUD에 관한 함수가 담긴 파일이다.
자세한 설명은 주석 참고
//DataController.swift
import Foundation
import CoreData
class DataController: ObservableObject {
///NSPersistentContainer : Data를 영구적으로 관리하려는 목적인듯 (앱 꺼켰해도 내가 수정한 대로 유지)
///name은 우리가 다루는 Core Data 파일 이름 그대로 넣기
let container = NSPersistentContainer(name: "FoodModel")
///그리고 여기도 이니셜라이저가 필요하다
init() {
///우리 데이터를 container라고 했는데, 이니셜라이저에서 container에서 PersistentStores를 load하겠다! 그리고 여기 error와 desc(부연설명)이 필요하기에 이 항목들 매개변수!
container.loadPersistentStores { desc, error in
if let error = error {
print("Failed to load the data \(error.localizedDescription)")
}
}
}
///가장 먼저 Save하는 함수를 적어줄 거다! Save를 못하면 CoreData를 사용하는 의미가 없기 때문
///context라는 전달인자가 필요하고, 이는 NSManageObjectContext 타입이다
func save(context: NSManagedObjectContext) {
do {
try context.save()
print("Data saved!!!")
} catch {
print("We could not save the data... sorry :(")
}
}
///항목 추가하는 Add 함수, name, calories, context라는 전달인자 필요 & 각각의 타입 선언
func addFood(name: String, calories: Double, context: NSManagedObjectContext) {
///Food : FoodModel의 Food라는 entities를 가리킴
///Food 틀을 따르는 이 context의 정보들을 food라는 상수에 담아서 선언하고 각각의 id, name, date, calories를 지칭해준다
let food = Food(context: context)
food.id = UUID()
food.name = name
food.date = Date()
food.calories = calories
///위에서 썼던 save 함수 발동해서 저장해주기
save(context: context)
}
///항목 수정하는 Edit 함수 & 각각 필요한 매개변수들의 타입 지정
func editFood(food: Food, name: String, calories: Double, context: NSManagedObjectContext) {
food.name = name
food.calories = calories
food.date = Date()
save(context: context)
}
}
*NSManagedObjectContext : 변할 가능성이 있는 데이터를 지정할 때 사용한다.
4. App 파일에서 Data를 이 플젝 전역에 뿌려주기
icalories 에서는 App 파일에서 전체에 데이터를 뿌려준다. 다른 Core Data도 마찬가지일까?
//icaloriesApp.swift
import SwiftUI
@main
struct icaloriesApp: App {
///@StateObject로 가져온다. DataController 파일에 있는 DataController class를 가져온다는 의미로 DataController() (이니셜라이저까지)
@StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
///모든 곳에서 이 데이터를 쓸 수 있게하는 코드 ex) @Environment(\.managedObjectContext)
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
.environment(\.managedObjectContext, dataController.container.viewContext) 코드를 쓰면 다른 View 파일에서
@Environment(\.managedObjectContext) var 변수명
형식으로 쓸 수 있다
View 파일에서 어떻게 받는지 보자
//ContentView.swift
import SwiftUI
struct ContentView: View {
@Environment(\.managedObjectContext) var managedObjContext
///database로부터 data를 쓰기 위해 FetchRequest 필요
///sortDescriptors : 가져오는 배열 방식 설정. 여기서는 date를 기준으로. + 역순으로 (날짜가 제일 적은 것부터 == 최신 것부터)
@FetchRequest(sortDescriptors: [SortDescriptor(\.date, order: .reverse)]) var food: FetchedResults<Food>
5. View 파일 생성 & 코딩
//ContentView.swift
import SwiftUI
struct ContentView: View {
@Environment(\.managedObjectContext) var managedObjContext
///database로부터 data를 쓰기 위해 FetchRequest 필요
///sortDescriptors : 가져오는 배열 방식 설정. 여기서는 date를 기준으로. + 역순으로 (날짜가 제일 적은 것부터 == 최신 것부터)
@FetchRequest(sortDescriptors: [SortDescriptor(\.date, order: .reverse)]) var food: FetchedResults<Food>
@State private var showingAddView = false
var body: some View {
NavigationView{
VStack(alignment: .leading){
Text("\(Int(totalCaloriesToday())) Kcal (Today)")
.foregroundColor(.gray)
.padding(.horizontal)
List{
ForEach(food){ food in
NavigationLink(destination: EditFoodView(food: food)){
VStack(alignment: .leading, spacing: 6){
HStack{
HStack{
VStack(alignment: .leading){
Text("\(food.name!)")
.bold()
Text("\(Int(food.calories))") + Text(" calories").foregroundColor(.red)
}
}
Spacer()
Text("\(calcTimeSince(date: food.date!))")
.foregroundColor(.gray)
.italic()
}
}
}
}.onDelete(perform: deleteFood)
}
.listStyle(.plain)
}
.navigationTitle("iCalories")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing){
// Button(Image(systemName: "plus.circle")){
// showingAddView.toggle()
// }
Button {
showingAddView.toggle()
} label: {
Image(systemName: "plus.circle")
}
}
ToolbarItem(placement: .navigationBarLeading){
///이거 개꿀. EditButton() 하나만 써놓으면 알아서 구현해준다
EditButton()
}
}
.sheet(isPresented: $showingAddView) {
AddFoodView()
}
}
.navigationViewStyle(.stack)
}
///offset: IndexSet 이 뭘까?
private func deleteFood(offsets: IndexSet) {
///delete는 애니메이션(해당 항목 옆으로 스와이프)이 포함된 것. 그러므로 따로 지정안해주고 {} 열어서 코드 쳐주기
withAnimation {
///forEach문 돌면서 해당 index 찾으면 delete 한다는 구문인 거 같은데, map이 정확히 뭐고, $0이 정확히 뭘까?
offsets.map {food[$0]}.forEach(managedObjContext.delete)
///삭제해젔으면 빠진 상태의 값 저장해주기.
DataController().save(context: managedObjContext)
}
}
func totalCaloriesToday() -> Double {
var caloriesToday: Double = 0
for item in food {
///이 구문 자체가 item의 date가 오늘이냐?! 를 판단해주는 아주 편한 코드이다. 필요할 때 갖다 쓰기
if Calendar.current.isDateInToday(item.date!) {
caloriesToday += item.calories
}
}
return caloriesToday
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//AddFoodView.swift
import SwiftUI
struct AddFoodView: View {
///이 wrapper들은 뭘까?
///Injecting Data때 선언해줬던 (icaloriesApp에서) wrapper를 여기에 쓴 거임
@Environment(\.managedObjectContext) var managedObjContext
///dismiss는 따로 파일에서 선언 안해줬는데 여기서 씀. 아마 내장함수 그런 개념인듯
@Environment(\.dismiss) var dismiss
@State private var name = ""
@State private var calories: Double = 0
var body: some View {
Form {
Section {
TextField("Food Name", text: $name)
VStack {
Text("Calories : \(Int(calories))")
Slider(value: $calories, in: 0...1000, step:10)
}
.padding()
HStack{
Spacer()
Button("Submit") {
///DataController() (이니셜라이저 포함)에서 addFood 함수를 쓴다!
DataController().addFood(name: name, calories: calories, context: managedObjContext)
///Submit 버튼 눌렀으면 당연히 이 창 닫혀야지. 그니까 dismiss() 호출
dismiss()
}
Spacer()
}
}
}
}
}
struct AddFoodView_Previews: PreviewProvider {
static var previews: some View {
AddFoodView()
}
}
@EditFoodView.swift
import SwiftUI
struct EditFoodView: View {
@Environment(\.managedObjectContext) var managedObjContext
@Environment(\.dismiss) var dismiss
///이 코드는 뭘까
var food : FetchedResults<Food>.Element
@State private var name = ""
@State private var calories: Double = 0
var body: some View {
Form{
Section{
TextField("\(food.name!)", text: $name)
///Edit에서 기존 값 불러오게 하는 코드
.onAppear{
name = food.name!
calories = food.calories
}
VStack{
Text("Calories : \(Int(calories))")
Slider(value: $calories, in: 0...1000, step: 10)
}
.padding()
HStack {
Spacer()
Button("Submit") {
DataController().editFood(food: food, name: name, calories: calories, context: managedObjContext)
dismiss()
}
Spacer()
}
}
///onAppear 코드가 Section {}을 감싸는 여기 위치에 있어도 결과는 같음
// .onAppear{
// name = food.name!
// calories = food.calories
// }
}
}
}
//TimeFormatting.swift
import Foundation
func calcTimeSince(date: Date) -> String {
let minutes = Int(-date.timeIntervalSinceNow)/60
let hours = minutes/60
let days = hours/24
if minutes < 120 {
return "\(minutes) minutes ago"
} else if minutes >= 120 && hours < 48 {
return "\(hours) hours ago"
} else {
return "\(days) days ago"
}
}
여러 View 파일들에 보면 쏠쏠한 코드들이 많다. 주석도 달려있으니 확인하면서 이해하자.
그리고 내가 어떤 코드를 어디에 썼는지 기억해서, 나중에 다시 쓸 일이 있으면 찾을 수 있게 하자.
코드를 다 직접 치진 못하더라도 어느 블로그 포스트에 뭐가 있었는지는 기억해야한다는 말.
참고 : iCalories 영상
'iOS > SwiftUI' 카테고리의 다른 글
[SwiftUI] iOS App Dev Tutorial에서 Data Update 파트 간단 정리 (0) | 2022.05.11 |
---|---|
[SwiftUI] Making Classes Observable (0) | 2022.05.11 |
[SwiftUI] Managing Data Flow Between Views (0) | 2022.05.10 |
[SwiftUI] Design Pattern : MVVM (0) | 2022.04.27 |