itisjustK
코딩과 사람 사는 이야기
itisjustK
전체 방문자
오늘
어제
  • 분류 전체보기 (207)
    • 일이삼사오육칠팔구십일이삼사오육칠팔구십일이삼사오육칠.. (0)
    • Web (43)
      • html & css (9)
      • django & python (15)
      • java script (9)
    • iOS (51)
      • Swift (42)
      • SwiftUI (5)
    • CS (25)
      • 자료구조 (6)
      • 운영체제 (3)
      • 데이터베이스 (9)
      • 네트워크 (7)
    • PS (34)
      • 알고리즘 & 자료구조 (0)
    • Life (36)
    • Retrospective (15)
    • Book (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • POSTECH
  • ios
  • CoreData
  • crud
  • SwiftUI
  • 어플
  • nosql
  • binding
  • 생활코딩 #이고잉 #HTML #코딩 #개발자
  • 개발자
  • 킨디
  • AppleDevloperAcademy
  • 독립서점
  • 점주
  • SWIFT
  • 세그멘테이션
  • 연결리스트
  • mongodb
  • CS
  • 생활코딩

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
itisjustK

코딩과 사람 사는 이야기

[SwiftUI] Core Data를 이용한 데이터 CRUD (iCalories)
iOS/SwiftUI

[SwiftUI] Core Data를 이용한 데이터 CRUD (iCalories)

2022. 5. 17. 12:24

그동안 궁금했던 점 : 앱을 껐다 켜도 내가 데이터를 추가, 수정, 삭제한 내역이 저장되게 하려면 어떻게 해야 할까? 

Core Data를 이용하면 이게 가능했다. 간단한 예시를 통해 Core Data의 핵심을 살펴보자 

 

  1. Data Model 파일 생성 
  2. Add Entity & Add Attribute
  3. Data Controller 파일 생성 ( Core Data를 불러오고 재가공하는 함수 코드가 담긴 파일. 일종의 ViewModel 역할을 하는듯? )
  4. App 파일에서 Data를 이 플젝 전역에 뿌려주기
  5. View 파일 생성 & 코딩

Create, Update, Delete

 


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
    'iOS/SwiftUI' 카테고리의 다른 글
    • [SwiftUI] iOS App Dev Tutorial에서 Data Update 파트 간단 정리
    • [SwiftUI] Making Classes Observable
    • [SwiftUI] Managing Data Flow Between Views
    • [SwiftUI] Design Pattern : MVVM
    itisjustK
    itisjustK

    티스토리툴바