[SwiftUI] CoreData & Relationships

Junyoung Park·2022년 8월 19일
1

SwiftUI

목록 보기
16/136
post-thumbnail

Core Data relationships, predicates, and delete rules in Xcode | Continued Learning #16

CoreData & Relationships

구현 목표

  • 코어 데이터 엔티티 내 릴레이션을 활용하기
  • 엔티티 간의 카디널리티(일 대 일, 다 대 일, 일 대 다, 다 대 다) 제약 조건 확인하기

구현 태스크

  1. 코어 데이터 릴레이션십을 추가한다.
  2. ObservableObject, StateObject 선언을 통해 데이터 변경 시 곧바로 UI를 리렌더링한다.
  3. 각 엔티티의 생성 조건에 따라 릴레이션을 추가한다.

핵심 코드

func addDepartment(businessName: String, departmentName: String) {
        if !businesses.filter{$0.name == businessName}.isEmpty {
            guard let business = businesses.filter{$0.name == businessName}.first else { return }
            if departments.filter{$0.name == departmentName}.isEmpty {
                let newDeparment = DepartmentEntity(context: manager.context)
                newDeparment.name = departmentName
                newDeparment.businesses = [business]
                if var curDepartments = business.departments {
                    curDepartments = curDepartments.adding(newDeparment) as NSSet
                    business.departments = curDepartments
                } else {
                    business.departments = [newDeparment]
                }
            } else {
                guard let curDepartment = departments.filter{$0.name == departmentName}.first else { return }
                if var curBusinesses = curDepartment.businesses {
                    curBusinesses = curBusinesses.adding(business) as NSSet
                    curDepartment.businesses = curBusinesses
                } else {
                    curDepartment.businesses = [business]
                }
                
            }
            save()
        }
    }
  • 릴레이션에 서로 추가할 엔티티 탐색
  • 일 대 일이라면 NSSet인 릴레이션을 그대로 오버라이드
  • 다 대 다 혹은 다 대 일, 즉 NSSet 릴레이션에 추가해야 할 때 adding 메소드를 통해 기존 릴레이션에 엔티티를 추가한 새로운 NSSet을 생성, 이후 오버라이드

소스 코드

class CoreDataManager {
    static let instance = CoreDataManager()
    let container: NSPersistentContainer
    let context: NSManagedObjectContext
    private init() {
        container = NSPersistentContainer(name: "CoreDataContainer")
        container.loadPersistentStores { description, error in
            if let error = error {
                print("ERROR LOADING CORE DATA")
                print(error.localizedDescription)
            } else {
                print("SUCCESSFULLY LOADING CORE DATA")
            }
        }
        context = container.viewContext
    }
    
    func save() {
        do {
            try context.save()
        } catch {
            print("ERROR SAVING CORE DATA")
            print(error.localizedDescription)
        }
    }
}
  • 코어 데이터를 담당하는 매니저 클래스
  • 싱글턴 패턴 구현
class CoreDataRelationshipViewModel: ObservableObject {
    let manager = CoreDataManager.instance
    @Published var businesses: [BusinessEntity] = []
    @Published var departments: [DepartmentEntity] = []
    @Published var employees: [EmployeeEntity] = []
    
    init() {
        getBusinesses()
        getDepartments()
        getEmployees()
    }
    
    func addBusiness(name: String) {
        guard !name.isEmpty else { return }
        if businesses.filter{$0.name == name}.isEmpty {
            let newBusiness = BusinessEntity(context: manager.context)
            newBusiness.name = name
            save()
        }
    }
    
    func addDepartment(businessName: String, departmentName: String) {
        if !businesses.filter{$0.name == businessName}.isEmpty {
            guard let business = businesses.filter{$0.name == businessName}.first else { return }
            if departments.filter{$0.name == departmentName}.isEmpty {
                let newDeparment = DepartmentEntity(context: manager.context)
                newDeparment.name = departmentName
                newDeparment.businesses = [business]
                if var curDepartments = business.departments {
                    curDepartments = curDepartments.adding(newDeparment) as NSSet
                    business.departments = curDepartments
                } else {
                    business.departments = [newDeparment]
                }
            } else {
                guard let curDepartment = departments.filter{$0.name == departmentName}.first else { return }
                if var curBusinesses = curDepartment.businesses {
                    curBusinesses = curBusinesses.adding(business) as NSSet
                    curDepartment.businesses = curBusinesses
                } else {
                    curDepartment.businesses = [business]
                }
                
            }
            save()
        }
    }
    
    func addEmployee(name: String, age: String, business: String, department: String) {
        guard !name.isEmpty && !age.isEmpty && !business.isEmpty && !department.isEmpty, let age = Int16(age) else { return }
        guard let business = businesses.filter{$0.name == business}.first, let department = departments.filter{$0.name == department}.first else { return }
        guard var curDepartments = business.departments, var curBusinesses = department.businesses, curDepartments.contains(department), curBusinesses.contains(business) else { return }
        guard employees.filter{$0.name == name && $0.age == age && $0.business == business && $0.department == department}.isEmpty else { return }
        
        let newEmployee = EmployeeEntity(context: manager.context)
        newEmployee.name = name
        newEmployee.age = age
        newEmployee.department = department
        newEmployee.business = business
        newEmployee.dateJoined = Date()
        
        if var businessEmployees = business.employees {
            businessEmployees = businessEmployees.adding(newEmployee) as NSSet
            business.employees = businessEmployees
        } else {
            business.employees = [newEmployee]
        }
        
        if var departmentEmployees = department.employees {
            departmentEmployees = departmentEmployees.adding(newEmployee) as NSSet
            department.employees = departmentEmployees
        } else {
            department.employees = [newEmployee]
        }
        
        save()
    }
    
    func getBusinesses() {
        let request = NSFetchRequest<BusinessEntity>(entityName: "BusinessEntity")
        do {
            self.businesses = try manager.context.fetch(request)
        } catch {
            print("ERROR FETCHING CORE DATA")
            print(error.localizedDescription)
        }
    }
    
    func getDepartments() {
        let request = NSFetchRequest<DepartmentEntity>(entityName: "DepartmentEntity")
        do {
            self.departments = try manager.context.fetch(request)
        } catch {
            print("ERROR FETCHING CORE DATA")
            print(error.localizedDescription)
        }
    }
    
    func getEmployees() {
        let request = NSFetchRequest<EmployeeEntity>(entityName: "EmployeeEntity")
        do {
            self.employees = try manager.context.fetch(request)
        } catch {
            print("ERROR FETCHING CORE DATA")
            print(error.localizedDescription)
        }
    }
    
    func save() {
        businesses.removeAll()
        departments.removeAll()
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.manager.save()
            self.getBusinesses()
            self.getDepartments()
            self.getEmployees()
        }
    }
    
}
  • UI 렌더링을 요청할 수 있도록 @Published로 선언된 로컬 데이터 배열
  • 데이터 CRUD 중 생성만 구현
  • 비즈니스, 부서, 직원 순서대로 입력 필요
  • 비즈니스 : 부서는 서로 다 대 다, 직원은 일 대 일, 직원과 다른 엔티티는 다 대 일 관계의 카디널리티 구현
  • 비즈니스는 다른 엔티티 없이 곧바로 생성 가능
  • 부서는 해당 비즈니스에 대해서 생성 가능
  • 직원은 해당 비즈니스, 부서가 있어야 생성 가능
  • 생성 함수 add...에서 현재 코어 데이터 내 저장된 오브젝트에 중복 데이터가 없는지 확인 가능
struct CoreDataRelationshipsBootCamp: View {
    @StateObject private var viewModel = CoreDataRelationshipViewModel()
    @State private var insertMode: Int = 0
    @State private var businessName: String = ""
    @State private var departmentName: String = ""
    @State private var employeeName: String = ""
    @State private var emplyeeAge: String = ""
    var body: some View {
        NavigationView {
            ScrollView {
                VStack {
                    HStack {
                        Button("Business") {
                            insertMode = 0
                            resetTextFieldText()
                        }
                        Button("Department") {
                            insertMode = 1
                            resetTextFieldText()
                        }
                        Button("Employee") {
                            insertMode = 2
                            resetTextFieldText()
                        }
                    }
                    switch insertMode {
                    case 0:
                        TextField("Add Business Name here...", text: $businessName)
                            .font(.headline)
                            .padding(.leading)
                            .frame(height: 55)
                            .background(Color.gray.opacity(0.1))
                            .cornerRadius(10)
                            .padding(.horizontal)
                    case 1:
                        TextField("Add Department Name here...", text: $departmentName)
                            .font(.headline)
                            .padding(.leading)
                            .frame(height: 55)
                            .background(Color.gray.opacity(0.1))
                            .cornerRadius(10)
                            .padding(.horizontal)
                        TextField("Department's Business", text: $businessName)
                            .font(.headline)
                            .padding(.leading)
                            .frame(height: 55)
                            .background(Color.gray.opacity(0.1))
                            .cornerRadius(10)
                            .padding(.horizontal)
                    default:
                        TextField("Add Employee's Name here...", text: $employeeName)
                            .font(.headline)
                            .padding(.leading)
                            .frame(height: 55)
                            .background(Color.gray.opacity(0.1))
                            .cornerRadius(10)
                            .padding(.horizontal)
                        TextField("Add Employee's Age here...", text: $emplyeeAge)
                            .font(.headline)
                            .padding(.leading)
                            .frame(height: 55)
                            .background(Color.gray.opacity(0.1))
                            .cornerRadius(10)
                            .padding(.horizontal)
                        TextField("Employee's Business", text: $businessName)
                            .font(.headline)
                            .padding(.leading)
                            .frame(height: 55)
                            .background(Color.gray.opacity(0.1))
                            .cornerRadius(10)
                            .padding(.horizontal)
                        TextField("Employee's Department", text: $departmentName)
                            .font(.headline)
                            .padding(.leading)
                            .frame(height: 55)
                            .background(Color.gray.opacity(0.1))
                            .cornerRadius(10)
                            .padding(.horizontal)
                    }
                    Button {
                        switch insertMode {
                        case 0:
                            viewModel.addBusiness(name: businessName)
                        case 1:
                            viewModel.addDepartment(businessName: businessName, departmentName: departmentName)
                        default:
                            viewModel.addEmployee(name: employeeName, age: emplyeeAge, business: businessName, department: departmentName)
                        }
                        resetTextFieldText()
                        
                    } label: {
                        Text("Submit")
                            .font(.headline)
                            .foregroundColor(.white)
                            .frame(height: 55)
                            .frame(maxWidth: .infinity)
                            .background(Color.pink)
                            .cornerRadius(10)
                            .padding(.horizontal)
                    }
                    .padding(.bottom)
                    ScrollView(.horizontal) {
                        HStack(alignment: .top) {
                            ForEach(viewModel.businesses) { business in
                                BusinesView(entity: business)
                            }
                        }
                    }
                    ScrollView(.horizontal) {
                        HStack(alignment: .top) {
                            ForEach(viewModel.departments) { department in
                                DepartmentView(entity: department)
                            }
                        }
                    }
                    ScrollView(.horizontal) {
                        HStack(alignment: .top) {
                            ForEach(viewModel.employees) { employee in
                                EmployeeView(entity: employee)
                            }
                        }
                    }
                    Spacer()
                }
                .navigationTitle("Relationships")
            }
        }
    }
    
    func resetTextFieldText() {
        businessName = ""
        departmentName = ""
        employeeName = ""
        emplyeeAge = ""
    }
}

struct BusinesView: View {
    let entity: BusinessEntity
    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            Text("name : \(entity.name ?? "Default name")")
                .bold()
            if let departments = entity.departments?.allObjects as? [DepartmentEntity] {
                Text("Departments:")
                    .bold()
                ForEach(departments) { department in
                    Text(department.name ?? "Default name")
                }
            }
            if let employees = entity.employees?.allObjects as? [EmployeeEntity] {
                Text("Employees:")
                    .bold()
                ForEach(employees) { employee in
                    Text(employee.name ?? "Default name")
                }
            }
        }
        .padding()
        .frame(maxWidth: 300, alignment: .leading)
        .background(Color.gray.opacity(0.5))
        .cornerRadius(10)
        .shadow(radius: 10)
    }
}

struct DepartmentView: View {
    let entity: DepartmentEntity
    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            Text("name : \(entity.name ?? "Default name")")
                .bold()
            if let businesses = entity.businesses?.allObjects as? [BusinessEntity] {
                Text("Business")
                    .bold()
                ForEach(businesses) { business in
                    Text(business.name ?? "Default name")
                }
            }
            if let employees = entity.employees?.allObjects as? [EmployeeEntity] {
                Text("Employees:")
                    .bold()
                ForEach(employees) { employee in
                    Text(employee.name ?? "Default name")
                }
            }
        }
        .padding()
        .frame(maxWidth: 300, alignment: .leading)
        .background(Color.green.opacity(0.5))
        .cornerRadius(10)
        .shadow(radius: 10)
    }
}

struct EmployeeView: View {
    let entity: EmployeeEntity
    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            Text("name: \(entity.name ?? "Default name")")
                .bold()
            Text("age: \(entity.age)")
                .bold()
            Text("Date Joined: \(entity.dateJoined ?? Date())")
                .bold()
            Text("Business:")
                .bold()
            Text(entity.business?.name ?? "Default name")
                .bold()
            Text("Department: ")
                .bold()
            Text(entity.department?.name ?? "Default name")
                .bold()
        }
        .padding()
        .frame(maxWidth: 300, alignment: .leading)
        .background(Color.blue.opacity(0.5))
        .cornerRadius(10)
        .shadow(radius: 10)
    }
}
  • UI 담당뷰. 상단 버튼을 통해 현재 입력하는 엔티티 종류를 결정할 수 있다.

구현 화면

profile
JUST DO IT

0개의 댓글