본문 바로가기

학습 노트/iOS (2021)

187. Entity Hierarchy, Relationships

Entity Hierarchy

Student   Employee
name = name
age = age
address = address
gender = gender
grade   salary
    course

위와 같은 Data Model에서 Student와 Employee의
name, age, address, gender 4가지의 Attribute가 동일하다.
이러한 경우 양쪽에 반복해 생성하기 보다는 공용으로 사용할 수 있도록 하는 것이 효율적이다.

이러한 것을 Entity 계층이라고 부르고, CoreData는 이것을 지원한다.

위와 같이 Person으로 공통된 Attribute를 분리하는 Entity를 생성하고,
필요한 Entity에서 이를 동일하게 사용하도록 구현하면

  • 데이터 모델 구현에 필요한 시간이 감소
  • 데이터 모델 자체의 간결화

두 이득을 볼 수 있다.

단, 동일한 Parent Entity를 가진 모든 Entity를 하나의 Table에 저장하는 방식을 사용한다.
따라서 Entity가 포함하는 Attribute의 수가 많을 수록, Child Entity와 저장된 데이터의 수가 많을 수록
눈에 띄게 성능이 저하되는 경향이 있다.
따라서 제한된 수준에서 사용하는 주의가 필요하다.

 

실습

Department.json
1 Management
2 Design
3 Development
4 HR
5 Planning
6 Finance
7 Sales
8 Public Relation
employee.json
age
address
name
salary
department (unknown = 0)

위와 같은 구조의 두 Json 파일에서 데이터를 읽어와
CoreData에 저장하는 것이 목표이다.

RawData.swift

해당 클래스는 Json 형식의 데이터를 파싱하고,
이를 배열로 반환하는 역할을 한다.

  • department.json 파일의 경로를 저장한다.
  • 파일의 경로를 통해 파일을 읽어 온다.
  • JSON 파일의 변환을 위해 JSONDecoder를 사용해 변환을 진행한다.
  • 변환된 데이터는 id와 name을 포함하는 DepartmentJSON 형식의 구조체 배열로 반환된다.

같은 방식으로 Employee.json 파일오 EmployeeJSON 형식의 구조체 배열로 변환해 반환한다.

Sample.xcdatamodeld

Parent Entity 생성

기존에 사용하던 PersonEntity의 Abstract Entity 속성을 변경한다.
이제는 추상 Entity로 동작한다.

Child Entity (Employee Entity) 생성 및 수정

새 Entity를 생성하고 이름을 Employee로 변경한다.
앞서 생성한 Person Entity의 Attribute를 상속 받을 수 있도록 Parent Entity로 Person을 지정한다.

생성한 Entity에 Attribute를 추가한다.
이름은 salary, 형식은 Decimal이다.
필수 Attribute가 될 수 있도록 Optional 속성은 해제한다.

Child Entity (Department Entity) 생성 및 수정

새 Entity를 생성하고 이름을 Department로 변경한다.

생성한 Entity에 Attribute를 추가한다.
이름은 name, 형식은 String이다.
필수 Attribute가 될 수 있도록 Optional 속성은 해제한다.

Entity간 Relation 생성

관계형 DB는 Record ID를 사용해 두 테이블을 연결한다.
하지만 Core Data는 ManagedObject를 사용해 직접 연결하므로 ID가 필요하지 않다.
따라서 JSON 파일에 존재하는 id는 데이터 import 시 한 번만 필요하다.

Core Data가 지원하는 관계는 두 가지다.

  • To-One Relationship
    1:1 연결
  • To-Many Relationship
    1:n 연결

특히 To-Many Relationship은 이번 경우와 같이
부서와 직원 사이의 관계에 적합하다.
이 경우 Department가 Source, Employee는 Destination이 된다.

Relationship 추가 (Department to Employee)

Relationship의 이름을 정할 때는 두 가지의 규칙을 따르면 좋다.

  • lowerCamelCase를 사용한다.
  • To-One Relationship은 단수형을, To-Many Relationship은 복수형을 사용한다.

속성중 inverse는 '역관계'를 의미한다.
Destination으로 지정한 Entity에서 Source로 접근하는 것이 가능해 진다.

Arrangement 속성은 데이터의 정렬 여부를 결정한다.
Enitity를 배열이 아닌 Set으로 반환하기 때문에 데이터의 순서가 존재하지 않는다.
따라서 정렬이 필요하다면 해당 속성을 사용해야한다.

Count 속성은 최솟값과 최댓값을 결정한다.
관계에 속할 수 있는 Enitity 의 수를 제한한다.

Delete Rule은 Source Entity 삭제시 처리 방식을 결정한다.

  • NoAction
    작업 없음 (관계 유지)
    Source가 삭제된 이후 Destination에서 Source로 접근하면 오류 발생
  • Deny
    Destination이 존재하면 Source를 삭제할 수 없다.
  • Cascade
    Destination을 함께 삭제
  • Nullify
    Source 삭제 후 관계를 함께 삭제

이름은 employees, 형식은 To-Many로 설정한다.
Inverse는 아직 설정할 수 없다.

Relationship 추가 (Employee to Department)

이름은 department, Destination을 Department,
Inverse를 employees, Type을 To-One으로 설정한다.

Inverse를 emplyees로 지정했기 때문에 상호간의 접근이 가능하다.

Data Model의 Graph Style

DataModel 편집기의 우측 하단에는 Graph Style을 변경할 수 있는 아이콘이 존재한다.

각각 Table과 Graph 스타일이라고 불린다.
Table 스타일은 지금까지 사용한 방식이고

Graph Style은 위와 같은 방식으로 Data Model을 시각화 해 표시한다.

속이 빈 화살표는 계층의 관계를 나타내고, 꺽쇠 화살표는 Relationship을 나타낸다.
꺽쇠 하나는 To-One 관계를, 두 개는 To-Many 관계를 나타낸다.
즉 다음과 같은 관계가 성립한다.

  • Employee에서 Department로는 To-One 관계이다.
  • Department에서 Employee로는 To-Many 관계이다.

Class 생성 방식

EmployeeEntity+CoreDataProperties

extension EmployeeEntity {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<EmployeeEntity> {
        return NSFetchRequest<EmployeeEntity>(entityName: "Employee")
    }

    @NSManaged public var salary: NSDecimalNumber?
    @NSManaged public var department: DepartmentEntity?

}

연결된 Relationship이 속성 department로 추가돼있다.
해당 속성을 통해 Department Entity에 접근할 수 있다.

DepartmentEntity+CoreDataProperties

extension DepartmentEntity {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<DepartmentEntity> {
        return NSFetchRequest<DepartmentEntity>(entityName: "Department")
    }

    @NSManaged public var name: String?
    @NSManaged public var employees: NSSet?

}

// MARK: Generated accessors for employees
extension DepartmentEntity {

    @objc(addEmployeesObject:)
    @NSManaged public func addToEmployees(_ value: EmployeeEntity)

    @objc(removeEmployeesObject:)
    @NSManaged public func removeFromEmployees(_ value: EmployeeEntity)

    @objc(addEmployees:)
    @NSManaged public func addToEmployees(_ values: NSSet)

    @objc(removeEmployees:)
    @NSManaged public func removeFromEmployees(_ values: NSSet)

}

마찬가지로 Department에 연결된 Relationship이 속성 employees로 추가돼있다.
형식이 NSSet인 것을 통해 반환형이 배열이 아님을 확인할 수 있다.

코드 아래의 extension은 Entity에 To-Many Relation이 추가된 경우 자동으로 생성된다.
메서드의 이름들은 Destination Entity의 이름이 접미로 사용된다.

 

Batch Insert

DataManager+BatchInsert
Json을 Entity로 변환하기

extension DataManager {
   func insertDepartment(from data: DepartmentJSON, in context: NSManagedObjectContext) -> DepartmentEntity? {
      var entity: DepartmentEntity?
      context.performAndWait {
         entity = DepartmentEntity(context: context)
         entity?.name = data.name
      }
      return entity
   }
   
   func insertEmployee(from data: EmployeeJSON, in context: NSManagedObjectContext) -> EmployeeEntity? {
      var entity: EmployeeEntity?
      context.performAndWait {
         entity = EmployeeEntity(context: context)
         entity?.name = data.name
         entity?.age = Int16(data.age)
         entity?.address = data.address
         entity?.salary = NSDecimalNumber(integerLiteral: data.salary)
      }
      
      return entity
   }
}

해당 코드에는 Json을 사용해 Entity를 생성하는 메서드가 선언돼 있다.

  • 새로운 Entity 생성
  • 속성 설정
  • 반환

의 메커니즘을 가진다.

BatchInsertViewController

class BatchInsertViewController: UIViewController {
   
   @IBOutlet weak var countLabel: UILabel!
   
   var importCount = 0
   
   @IBAction func batchInsert(_ sender: UIButton) {
      sender.isEnabled = false
      importCount = 0
      
      DispatchQueue.global().async {
         let start = Date().timeIntervalSinceReferenceDate
         
         
         DispatchQueue.main.async {
            sender.isEnabled = true
            self.countLabel.text = "Done"
            
            let end = Date().timeIntervalSinceReferenceDate
            print(end - start)
         }
      }
   }
}

해당 코드에 선언돼있는 batchInsert 메서드는
Main Thread의 Blocking을 막기 위해 Global Queue를 사용하는 것이 특징이다.
Global Queue를 사용할 때는 인터페이스 업데이트 관련 코드는 반드시 Main Thread로 변경해 줘야 함을 잊지 말자.

      DispatchQueue.global().async {
         let start = Date().timeIntervalSinceReferenceDate
		  
		  let departmentList = DepartmentJSON.parsed()
		  let employeeList = EmployeeJSON.parsed()
         
         
         DispatchQueue.main.async {
            sender.isEnabled = true
            self.countLabel.text = "Done"
            
            let end = Date().timeIntervalSinceReferenceDate
            print(end - start)
         }
      }

우선 각각 존재하는 Json 파일에서 사용할 데이터를 배열로 반환 받는다.
이전에 선언해 둔 parsed 메서드를 사용해 각각에 맞는 구조체 배열로 변환한다.

      DispatchQueue.global().async {
         let start = Date().timeIntervalSinceReferenceDate
		  
		  let departmentList = DepartmentJSON.parsed()
		  let employeeList = EmployeeJSON.parsed()
         
		  let context = DataManager.shared.mainContext
		  context.performAndWait {
			  for dept in departmentList {
				  
			  }
		  }
         
         DispatchQueue.main.async {
            sender.isEnabled = true
            self.countLabel.text = "Done"
            
            let end = Date().timeIntervalSinceReferenceDate
            print(end - start)
         }
      }

새 Context를 생성하고, 해당 Context는 작업이 끝나고 반환 될 수 있도록
performAndWait 속성을 사용한다.

departmentList로 전달 받은 department 데이터를 순서대로 열거해서

      DispatchQueue.global().async {
         let start = Date().timeIntervalSinceReferenceDate
		  
		  let departmentList = DepartmentJSON.parsed()
		  let employeeList = EmployeeJSON.parsed()
         
		  let context = DataManager.shared.mainContext
		  context.performAndWait {
			  for dept in departmentList {
				  guard let newDeptEntity = DataManager.shared.insertDepartment(from: dept, in: context) else {
					  fatalError()
				  }
			  }
		  }
         
         DispatchQueue.main.async {
            sender.isEnabled = true
            self.countLabel.text = "Done"
            
            let end = Date().timeIntervalSinceReferenceDate
            print(end - start)
         }
      }

insertDepartment 메서드를 사용해 department를 추가하고 이를 바인딩한다.

      DispatchQueue.global().async {
         let start = Date().timeIntervalSinceReferenceDate
		  
		  let departmentList = DepartmentJSON.parsed()
		  let employeeList = EmployeeJSON.parsed()
         
		  let context = DataManager.shared.mainContext
		  context.performAndWait {
			  for dept in departmentList {
				  guard let newDeoptEntity = DataManager.shared.insertDepartment(from: dept, in: context) else {
					  fatalError()
				  }
				  
				  let employeesInDept = employeeList.filter {
					  $0.department == dept.id
				  }
			  }
		  }
         
         DispatchQueue.main.async {
            sender.isEnabled = true
            self.countLabel.text = "Done"
            
            let end = Date().timeIntervalSinceReferenceDate
            print(end - start)
         }
      }

employeeList에서 department가 dept의 id와 같은 데이터를 필터링한다.

      DispatchQueue.global().async {
         let start = Date().timeIntervalSinceReferenceDate
		  
		  let departmentList = DepartmentJSON.parsed()
		  let employeeList = EmployeeJSON.parsed()
         
		  let context = DataManager.shared.mainContext
		  context.performAndWait {
			  for dept in departmentList {
				  guard let newDeptEntity = DataManager.shared.insertDepartment(from: dept, in: context) else {
					  fatalError()
				  }
				  
				  let employeesInDept = employeeList.filter {
					  $0.department == dept.id
				  }
				  
				  for emp in employeesInDept {
					  guard let newEmployeeEntity = DataManager.shared.insertEmployee(from: emp, in: context) else {
						  fatalError()
					  }
				  }
			  }
		  }
         
         DispatchQueue.main.async {
            sender.isEnabled = true
            self.countLabel.text = "Done"
            
            let end = Date().timeIntervalSinceReferenceDate
            print(end - start)
         }
      }

필터링된 employeeList를 열거해 새 employee를 추가한다.
이미 선언해 둔 insertEmployee 메서드를 사용한다.

      DispatchQueue.global().async {
         let start = Date().timeIntervalSinceReferenceDate
		  
		  let departmentList = DepartmentJSON.parsed()
		  let employeeList = EmployeeJSON.parsed()
         
		  let context = DataManager.shared.mainContext
		  context.performAndWait {
			  for dept in departmentList {
				  guard let newDeptEntity = DataManager.shared.insertDepartment(from: dept, in: context) else {
					  fatalError()
				  }
				  
				  let employeesInDept = employeeList.filter {
					  $0.department == dept.id
				  }
				  
				  for emp in employeesInDept {
					  guard let newEmployeeEntity = DataManager.shared.insertEmployee(from: emp, in: context) else {
						  fatalError()
					  }
					  
					  newDeptEntity.addToEmployees(newEmployeeEntity)
					  newEmployeeEntity.department = newDeptEntity
				  }
			  }
		  }
         
         DispatchQueue.main.async {
            sender.isEnabled = true
            self.countLabel.text = "Done"
            
            let end = Date().timeIntervalSinceReferenceDate
            print(end - start)
         }
      }

이렇게 생성된 두 Entity를 연결한다.

Department에 Employee를 연결할 때는 class에서 생성된 extension인 addToEmployees 메서드를 사용한다.
Employee에 Department를 연결할 때는 Employee의 department 속성에 직접 지정하면 된다.

      DispatchQueue.global().async {
         let start = Date().timeIntervalSinceReferenceDate
		  
		  let departmentList = DepartmentJSON.parsed()
		  let employeeList = EmployeeJSON.parsed()
         
		  let context = DataManager.shared.mainContext
		  context.performAndWait {
			  for dept in departmentList {
				  guard let newDeptEntity = DataManager.shared.insertDepartment(from: dept, in: context) else {
					  fatalError()
				  }
				  
				  let employeesInDept = employeeList.filter {
					  $0.department == dept.id
				  }
				  
				  for emp in employeesInDept {
					  guard let newEmployeeEntity = DataManager.shared.insertEmployee(from: emp, in: context) else {
						  fatalError()
					  }
					  
					  newDeptEntity.addToEmployees(newEmployeeEntity)
					  newEmployeeEntity.department = newDeptEntity
					  
					  self.importCount += 1
					  
					  DispatchQueue.main.async {
						  self.countLabel.text = "\(self.importCount)"
					  }
				  }
			  }
		  }
         
         DispatchQueue.main.async {
            sender.isEnabled = true
            self.countLabel.text = "Done"
            
            let end = Date().timeIntervalSinceReferenceDate
            print(end - start)
         }
      }

insert 작업이 종료 됐으므로 importCount를 증가시키고 인터페이스를 업데이트한다.
이 때 인터페이스 업데이트는 Main Thread에서 진행될 수 있도록 주의한다.

      DispatchQueue.global().async {
         let start = Date().timeIntervalSinceReferenceDate
		  
		  let departmentList = DepartmentJSON.parsed()
		  let employeeList = EmployeeJSON.parsed()
         
		  let context = DataManager.shared.mainContext
		  context.performAndWait {
			  for dept in departmentList {
				  guard let newDeptEntity = DataManager.shared.insertDepartment(from: dept, in: context) else {
					  fatalError()
				  }
				  
				  let employeesInDept = employeeList.filter {
					  $0.department == dept.id
				  }
				  
				  for emp in employeesInDept {
					  guard let newEmployeeEntity = DataManager.shared.insertEmployee(from: emp, in: context) else {
						  fatalError()
					  }
					  
					  newDeptEntity.addToEmployees(newEmployeeEntity)
					  newEmployeeEntity.department = newDeptEntity
					  
					  self.importCount += 1
					  
					  DispatchQueue.main.async {
						  self.countLabel.text = "\(self.importCount)"
					  }
				  }
			  }
			  
			  do {
				  try context.save()
			  } catch {
				  print(error.localizedDescription)
			  }
		  }
         
         DispatchQueue.main.async {
            sender.isEnabled = true
            self.countLabel.text = "Done"
            
            let end = Date().timeIntervalSinceReferenceDate
            print(end - start)
         }
      }

작업이 완료 됐으므로 수정한 Context를 CoreData에 반영한다.

지금과 같이 큰 데이터를 저장할 때는 작업이 전부 완료된 다음 한 번에 저장하는 것은 비효율적이다.

      DispatchQueue.global().async {
         let start = Date().timeIntervalSinceReferenceDate
		  
		  let departmentList = DepartmentJSON.parsed()
		  let employeeList = EmployeeJSON.parsed()
         
		  let context = DataManager.shared.mainContext
		  context.performAndWait {
			  for dept in departmentList {
				  guard let newDeptEntity = DataManager.shared.insertDepartment(from: dept, in: context) else {
					  fatalError()
				  }
				  
				  let employeesInDept = employeeList.filter {
					  $0.department == dept.id
				  }
				  
				  for emp in employeesInDept {
					  guard let newEmployeeEntity = DataManager.shared.insertEmployee(from: emp, in: context) else {
						  fatalError()
					  }
					  
					  newDeptEntity.addToEmployees(newEmployeeEntity)
					  newEmployeeEntity.department = newDeptEntity
					  
					  self.importCount += 1
					  
					  DispatchQueue.main.async {
						  self.countLabel.text = "\(self.importCount)"
					  }
				  }
				  
				  do {
					  try context.save()
				  } catch {
					  print(error.localizedDescription)
				  }
			  }
		  }
         
         DispatchQueue.main.async {
            sender.isEnabled = true
            self.countLabel.text = "Done"
            
            let end = Date().timeIntervalSinceReferenceDate
            print(end - start)
         }
      }

따라서 위와 같이 Department 생성이 완료 되면 반영하도록 변경하는 것도 가능하다.

      DispatchQueue.global().async {
         let start = Date().timeIntervalSinceReferenceDate
		  
		  let departmentList = DepartmentJSON.parsed()
		  let employeeList = EmployeeJSON.parsed()
         
		  let context = DataManager.shared.mainContext
		  context.performAndWait {
			  for dept in departmentList {
				  guard let newDeptEntity = DataManager.shared.insertDepartment(from: dept, in: context) else {
					  fatalError()
				  }
				  
				  let employeesInDept = employeeList.filter {
					  $0.department == dept.id
				  }
				  
				  for emp in employeesInDept {
					  guard let newEmployeeEntity = DataManager.shared.insertEmployee(from: emp, in: context) else {
						  fatalError()
					  }
					  
					  newDeptEntity.addToEmployees(newEmployeeEntity)
					  newEmployeeEntity.department = newDeptEntity
					  
					  self.importCount += 1
					  
					  DispatchQueue.main.async {
						  self.countLabel.text = "\(self.importCount)"
					  }
				  }
				  
				  do {
					  try context.save()
				  } catch {
					  print(error.localizedDescription)
				  }
			  }
			  
			  let otherEmployees = employeeList.filter {
				  $0.department == 0
			  }
			  
			  for emp in employeeList {
				  _ = DataManager.shared.insertEmployee(from: emp, in: context)
				  
				  self.importCount += 1
				  
				  DispatchQueue.main.async {
					  self.countLabel.text = "\(self.importCount)"
				  }
			  }
			  
			  do {
				  try context.save()
			  } catch {
				  print(error.localizedDescription)
			  }
		  }
         
         DispatchQueue.main.async {
            sender.isEnabled = true
            self.countLabel.text = "Done"
            
            let end = Date().timeIntervalSinceReferenceDate
            print(end - start)
         }
      }

Department에 속하지 않는 Employee도 department를 0으로 설정한 뒤
같은 방식으로 UI를 업데이트 하고, 저장할 수 있도록 구현한다.
단, 이 경우 연결할 Department가 없으므로 관계를 연결하는 과정이 생략된다.

위의 모든 과정들을 완료하는데 통상 0.5초 미만이 걸린다.

현재 방식은 mainContext를 사용하기 때문에 MainThread를 Blocking할 가능성이 존재한다.
따라서 Background Context를 사용하는 것이 좋다.

 

Relationship

DataManager+Department.swift

extension DataManager {
   func fetchDepartment() -> [DepartmentEntity] {
      var list = [DepartmentEntity]()
      
      mainContext.performAndWait {
         let request: NSFetchRequest<DepartmentEntity> = DepartmentEntity.fetchRequest()
         
         let sortByName = NSSortDescriptor(key: #keyPath(DepartmentEntity.name), ascending: true)
         request.sortDescriptors = [sortByName]
         
         do {
            list = try mainContext.fetch(request)
         } catch {
            fatalError(error.localizedDescription)
         }
      }
      
      return list
   }
}

해당 코드에는 Department 데이터를 읽어 배열로 반환하는 메서드가 선언돼있다.
fetchDepartment 메서드이다.

Datamanager_Employee.swift

extension DataManager {
   func fetchNotAssignedEmployee() -> [EmployeeEntity] {
      var list = [EmployeeEntity]()
      
      mainContext.performAndWait {
         let request: NSFetchRequest<EmployeeEntity> = EmployeeEntity.fetchRequest()
         
         let filterByDept = NSPredicate(format: "department == nil")
         request.predicate = filterByDept
         
         let sortByName = NSSortDescriptor(key: #keyPath(EmployeeEntity.name), ascending: true)
         request.sortDescriptors = [sortByName]
         
         do {
            list = try mainContext.fetch(request)
         } catch {
            fatalError(error.localizedDescription)
         }
      }
      
      return list
   }
}

해당 코드에는 Department에 속하지 않는 Employee 데이터를 배열로 반환하는 메서드가 선언돼있다.
fetchNotAssignedEmployee 메서드이다.

RDepartmentListViewController

class RDepartmentListViewController: UIViewController {
   
   @IBOutlet weak var listTableView: UITableView!
   
   var list = [DepartmentEntity]()
   
   override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
      if let cell = sender as? UITableViewCell, let indexPath = listTableView.indexPath(for: cell) {
         if let vc = segue.destination as? REmployeeListViewController {
            vc.department = list[indexPath.row]
         }
      }
   }   
   
   override func viewWillAppear(_ animated: Bool) {
      super.viewWillAppear(animated)
      
      
   }
}

extension RDepartmentListViewController: UITableViewDataSource {
   func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return list.count
   }
   
   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      
      
      return cell
   }
}

우선 NSManagementObject 형식의 배열이던 list를 DepartmentEntity 형식으로 변경한다.

   override func viewWillAppear(_ animated: Bool) {
      super.viewWillAppear(animated)
      
	   list = DataManager.shared.fetchDepartment()
	   listTableView.reloadData()
   }

View가 표시되면 list 배열에 Department 데이터를 저장하고,
TableView를 새로고침한다.

   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      
	   let dept = list[indexPath.row]
	   
	   cell.textLabel?.text = dept.name
	   cell.detailTextLabel?.text = "\(dept.employees?.count ?? 0)"
      
      return cell
   }

TableView에는 textLabel에 Department의 이름을,
detailTextLabel에는 속한 employee의 수를 표시한다.

   override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
      if let cell = sender as? UITableViewCell, let indexPath = listTableView.indexPath(for: cell) {
         if let vc = segue.destination as? REmployeeListViewController {
            vc.department = list[indexPath.row]
         }
      }
   }

또한 특정 Cell을 선택하면 해당 행의 Department 데이터를 REmployeeListViewController에 전달한다.

REmployeeListViewController

class REmployeeListViewController: UIViewController {
   var department: DepartmentEntity?
   var list = [EmployeeEntity]()
   
   @IBOutlet weak var listTableView: UITableView!
   
   override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
      if let nav = segue.destination as? UINavigationController, let vc = nav.viewControllers.first as? RDepartmentComposerTableViewController {
         vc.department = department
      }
   }
   
   
   override func viewWillAppear(_ animated: Bool) {
      super.viewWillAppear(animated)
      
      
   }
}

extension REmployeeListViewController: UITableViewDataSource {
   func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return list.count
   }
   
   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      
      
      return cell
   }
   
   func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
      switch editingStyle {
      case .delete:
         break
      default:
         break
      }
   }
}

기존에 형식으로 지정돼있던 NSManagedObject 대신
DepartmentEntity와 EmployeeEntity로 변경한다.

   override func viewWillAppear(_ animated: Bool) {
      super.viewWillAppear(animated)
      
	   guard let employeeList = department?.employees?.allObjects as? [EmployeeEntity] else {
		   return
	   }
      
   }

전달받은 Department의 employee의 형식이 Set이기 때문에 바로는 사용할 수 없다.
정렬해 사용할 수 있도록 EmployeeEntity 형식의 배열로 변환한다.

   override func viewWillAppear(_ animated: Bool) {
      super.viewWillAppear(animated)
      
	   guard let employeeList = department?.employees?.allObjects as? [EmployeeEntity] else {
		   return
	   }
	   list = employeeList.sorted {
		   $0.name! < $1.name!
	   }
	   listTableView.reloadData()
   }

sorted 메서드를 사용해 알파벳 순으로 정렬한 뒤 TableView를 새로고침한다.

   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      
	   let employee = list[indexPath.row]
	   cell.textLabel?.text = employee.name
	   cell.detailTextLabel?.text = employee.address
      
      return cell
   }

준비된 데이터를 TableView에 표시한다.
textLabel에는 employee의 이름을, detailTextlabel에는 주소를 표시한다.

   func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
      switch editingStyle {
      case .delete:
		  guard let dept = department else {
			  fatalError()
		  }
		  let employee = list[indexPath.row]
      default:
         break
      }
   }

삭제 기능을 위해 대상 department와 employee를 특정한다.

   func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
      switch editingStyle {
      case .delete:
		  guard let dept = department else {
			  fatalError()
		  }
		  let employee = list[indexPath.row]
		  
		  dept.removeFromEmployees(employee)
		  employee.department = nil
      default:
         break
      }
   }

Source인 department에서 employee를 제거하고,
employee의 department 속성을 nil로 변경한다.

   func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
      switch editingStyle {
      case .delete:
		  guard let dept = department else {
			  fatalError()
		  }
		  let employee = list[indexPath.row]
		  
		  dept.removeFromEmployees(employee)
		  employee.department = nil
		  
		  DataManager.shared.saveMainContext()
      default:
         break
      }
   }

변경된 Context를 CoreData에 반영하고

   func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
      switch editingStyle {
      case .delete:
		  guard let dept = department else {
			  fatalError()
		  }
		  let employee = list[indexPath.row]
		  
		  dept.removeFromEmployees(employee)
		  employee.department = nil
		  
		  DataManager.shared.saveMainContext()
		  
		  list.remove(at: indexPath.row)
		  tableView.deleteRows(at: [indexPath], with: .automatic)
      default:
         break
      }
   }

list에서 데이터를 삭제한 뒤 TableView의 Cell을 삭제한다.

새 Relationship 생성하기

REmployeeListViewController

   override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
      if let nav = segue.destination as? UINavigationController, let vc = nav.viewControllers.first as? RDepartmentComposerTableViewController {
         vc.department = department
      }
   }

해당 View에서 + 버튼을 누르면 prepare 메서드가 호출된다.
해당 메서드는 선택된 대상을 전달한다.

RDepartmentComposerTableViewController

class RDepartmentComposerTableViewController: UITableViewController {
   
   var department: DepartmentEntity?
   var list = [EmployeeEntity]()
   
   @IBAction func cancel(_ sender: Any) {
      dismiss(animated: true, completion: nil)
   }
   
   @IBAction func save(_ sender: Any) {
      guard let targetDept = department else {
         fatalError()
      }
      
      
      dismiss(animated: true, completion: nil)
   }
   
   override func viewDidLoad() {
      super.viewDidLoad()
      
      
   }
}


extension RDepartmentComposerTableViewController {
   override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return list.count
   }
   
   override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      
      cell.textLabel?.text = list[indexPath.row].value(forKey: "name") as? String
      cell.detailTextLabel?.text = list[indexPath.row].value(forKey: "address") as? String
      
      return cell
   }
   
   override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      let cell = tableView.cellForRow(at: indexPath)
      cell?.accessoryType = .checkmark
   }
   
   override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
      let cell = tableView.cellForRow(at: indexPath)
      cell?.accessoryType = .none
   }
}

마찬가지로 NSManagedObject 형식으로 되어있던 epartment와 list의 형식을
DepartmentEntity와 EmployeeEntity로 변경한다.

   override func viewDidLoad() {
      super.viewDidLoad()
      
	   list = DataManager.shared.fetchNotAssignedEmployee()
	   tableView.reloadData()
      
   }

view가 표시되면 Department가 지정되지 않은 Employee 데이터를 불러오고,
TableView를 새로고침한다.

save 메서드

   @IBAction func save(_ sender: Any) {
      guard let targetDept = department else {
         fatalError()
      }
      
	   guard let selectedIndexPaths = tableView.indexPathsForSelectedRows else {
		   fatalError()
	   }
      
      dismiss(animated: true, completion: nil)
   }

추가될 department를 지정하고,
선택된 직원들의 indexpath를 저장한다.

   @IBAction func save(_ sender: Any) {
      guard let targetDept = department else {
         fatalError()
      }
      
	   guard let selectedIndexPaths = tableView.indexPathsForSelectedRows else {
		   fatalError()
	   }
	   
	   let selectedEmployees = selectedIndexPaths.map {
		   list[$0.row]
	   }
      
      dismiss(animated: true, completion: nil)
   }

indexpath에 대응하는 배열로 저장한 뒤

   @IBAction func save(_ sender: Any) {
      guard let targetDept = department else {
         fatalError()
      }
      
	   guard let selectedIndexPaths = tableView.indexPathsForSelectedRows else {
		   fatalError()
	   }
	   
	   let selectedEmployees = selectedIndexPaths.map {
		   list[$0.row]
	   }
	   
	   for employee in selectedEmployees {
		   targetDept.addToEmployees(employee)
		   employee.department = targetDept
	   }
	   
	   DataManager.shared.saveMainContext()
      
      dismiss(animated: true, completion: nil)
   }

배열을 순회하며 Relation을 생성한 뒤 Context를 CoreData에 반영한다.

'학습 노트 > iOS (2021)' 카테고리의 다른 글

190. Predicate  (0) 2022.03.24
187 ~ 189. Fetch Request  (0) 2022.02.25
186. Managed Object and Managed Object Context  (0) 2022.02.12
185. CoreData  (0) 2022.02.05
183 ~ 184. NSCoding and Codable  (0) 2022.01.27