iOS/데이터베이스 조작

CoreData를 이용해서 카테고리 계획 앱 만들기

소재훈 2022. 2. 4. 22:09

Todoey목록에서 카테고리를 등록하고, 각 카테고리마다 Item들을 저장하는 앱을 만들어보자. Item에 대한 내용은 이전에 설명하였기 때문에, 이번에는 Category뷰를 만들어보면서 이전에 살펴보았던 내용을 함께 살펴보고, 두 개의 데이터를 one to many나 many to one의 관계로 연결하고, 관계(Relation)의 이름을 통해 관계를 가지는 데이터를 어떻게 사용하는지 알아보자.

 

데이터 Relation만들기

먼저 Category라는 새로운 엔티티를 만들고, Item엔티티와 관계(Relation)를 만들어준다. control(⌃)을 누른 채로 마우스 드래그하면, 관계를 만들 수 있으며, 각 엔티티에서 관계를 어떻게 부를지 Relation의 이름도 설정할 수 있다.

Category와 Item엔티티는 one-to-many관계를 가진다. 아이템은 하나의 카테고리만 가질 수 있지만, 카테고리는 여러 개의 Item을 가질 수 있기 때문이다. Category는 name이라는 String속성을 하나 가진다. Item과의 Relation를 Items라고 부르며, Item엔티티 에서는 Category와의 Relation을 parentCategory라고 부른다.

 

기본적인 변수 정의하기

CategoryViewController를 만들고, 

class CategoryViewController: UITableViewController {

    var categories = [Category]()
    let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    ...

Category엔티티를 담을 배열(출력될 것)과 CoreData에서 임시 저장소로 사용되는 context를 정의해준다.

 

테이블 뷰 출력하기

먼저 테이블 뷰에서 Cell을 출력하는 Datasource Method를 구현해보자.

UITableViewController의 메서드인

tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int

 

이 메서드는 출력한 셀(Cell)의 개수를 리턴하며, 이 메서드에서 리턴된 수만큼 아래의 메서드가 호출된다.

tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

이 메서드에서 사용될 cell을 만든 후 반환하면, tableView(_ tableView: numberOfRowsInSection section:) 함수에서 반환된 수만큼의 cell이 출력된다.

 

우리는 category의 수만큼의 cell을 출력하고, 각 cell에는 category의 name 프로퍼티가 담기므로 다음과 같이 작성한다.

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return categories.count
    }
    
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "CategoryCell", for: indexPath)
    cell.textLabel?.text = categories[indexPath.row].name

    return cell
}

cell을 만들 때는 재사용 가능한 셀을 뽑아와서 사용하기 위해, tableView에서 dequeueReusableCell메서드를 통해 호출한다. withIdentifier에는 셀의 이름이 들어가며, 몇 번째 셀인지를 표시하기 위해서 for에는 tableView메서드의 매개변수로 주어진 IndexPath가 주어진다.

 

새로운 카테고리 추가하기

앱의 우상단의 Add버튼을 눌러 새로운 카테고리를 출력하는 과정을 살펴보자.

storyboard에서 assistant를 사용해 버튼에서 control(⌃)을 누른 채로 드래그하면, 버튼의 액션을 정의할 수 있는 것은 쉬울 것이다.

우리가 원하는 것은 Add버튼을 눌렀을 때, alert창이 나와 새로운 Category의 이름을 입력받고, tableView에 추가해 주는 것이다.

위처럼 alert창을 띄우고, 텍스트 필드에 입력하고, 버튼을 눌러 액션을 추가하기 위해서는, alert와 action을 따로 정의하고 alert에 action을 등록해주어야 한다.

@IBAction func addButtonPressed(_ sender: UIBarButtonItem) {
    var textField = UITextField()
    let alert = UIAlertController(title: "새로운 카테고리를 추가", message: "", preferredStyle: .alert)
    let action = UIAlertAction(title: "카테고리 추가", style: .default) { action in
        let newCategory = Category(context: self.context)
        newCategory.name = textField.text
        self.categories.append(newCategory)

        self.saveCategories()
    }

    alert.addTextField { field in
        textField = field
        textField.placeholder = "새로운 카테고리 추가"
    }

    alert.addAction(action)
    present(alert, animated: true, completion: nil)
}

textField 변수는 입력받은 내용을 바탕으로 새로운 Category의 name프로퍼티를 설정하기 위해서 사용된다.

UIAlertController클래스를 통해서 알림을 만들고, 그 알림의 정보를 alert변수에 담고 있다. title에는 굵은 글씨로 알려줄 내용이 담기며, message에는 알림에 담길 메시지의 내용이 들어간다. title의 내용 밑에 작은 글씨로 표현된다. 지금은 사용하지 않았지만 작성한다면 아래와 같이 표시된다.

message 매개변수를 사용하였을 경우

alert창의 "카테고리 추가"버튼은 action에 해당한다. 위와 같이 action을 추가하고 싶다면, UIActionController클래스를 통해서 action을 정의하고, UIAlertController의 addAction() 메서드를 통해서 등록해주어야 한다. 버튼의 텍스트를 title을 통해서 제공하고, 어떠한 액션이 수행되는지 클로저를 통해서 작성되어 있는 것을 볼 수 있다.

textField의 text속성의 값을 새로 만든 Category객체의 name프로퍼티에 넣어주고, categoies 배열에 원소를 추가해준다.

새로 Category객체를 만들어 줄 때, 직접 CoreData에 데이터를 넣기 전의 임시공간인 context를 등록해주어야 하고, 데이터를 저장할 때는 context.save()를 사용해 CoreData에 데이터를 저장할 수 있다.

 

위에서 사용된 saveCategories는 다음과 같다.

func saveCategories() {
    do {
        try context.save()
    } catch {
        print("Error saving context, \(error)")
    }

    self.tableView.reloadData()
}

데이터를 저장한 후 reloadData() 메서드를 통해 테이블 뷰의 데이터를 새로고침 해주면, 데이터가 추가된 효과를 보여줄 수 있다.

 

이렇게 구현한 알림은 present() 메서드를 사용해 화면에 보여줄 수 있다.

 

카테고리를 눌렀을 때, 연관된 Items 보여주기

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        //when this method is called, takes the user to ItemViewController
        performSegue(withIdentifier: "goToItems", sender: self)
    }
    
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let destinationVC = segue.destination as! ToDoListViewController
    if let indexPath = tableView.indexPathForSelectedRow {
        destinationVC.selectedCategory = categories[indexPath.row]
    }
}

테이블 뷰가 선택되었을 때, 연관된 아이템을 보여주기 위해서, tableView(_ tableView: didSelectRowAt indexpath:) 메서드를 사용하였다. 지정된 segue로 이동하기 위해서 performSegue메서드를 사용하고, segue를 통해서 뷰를 이동하기 전에 준비할 것들은 prepare메서드에서 수행할 수 있다. 목적지 뷰의 selectedCategory프로퍼티는 선택된 카테고리와 relation이 있는 Item의 목록만을 보여주기 위해서 사용한다.

 

그러면 이제, Items의 뷰를 나타내는 ItemViewController의 loadItems()에서, 관련된 Items만 뽑아내기 위해서 NSPredicate를 사용한다. 매개변수에도 NSPredicate? 의 매개변수를 받고 있는데 이는 데이터를 로드할 때마다 추가적으로 필요한 조건이 있을 수 있기 때문이다. 예를 들어 검색을 할 경우, 검색어에 해당하는 Item + 현재 들어와 있는 카테고리과 관련된 아이템의 두 가지 조건이 필요하다.

predicate가 nil이 아니라면, 조건이 이미 존재한다는 것이므로 이때는 NSCompoundPredicate(andPredicateWithSubpredicates:)를 통해서 여러 개의 조건을 한 번에 할당해 줄 수 있다.

var selectedCategory: Category? {
    didSet {
        loadItems()
    }
}
...

func loadItems(with request: NSFetchRequest<Item> = Item.fetchRequest(), predicate: NSPredicate? = nil) {
    let categoryPredicate = NSPredicate(format: "parentCategory.name MATCHES %@", selectedCategory!.name!)

    if let additionalPredicate = predicate {
        request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [categoryPredicate, additionalPredicate])
    } else {
        request.predicate = categoryPredicate
    }
    do {
        itemArray = try context.fetch(request)
    } catch {
        print("Error fetching data from context \(error)")
    }

    tableView.reloadData()
}