My Todo List
새로운 주차가 시작 됐고, 또 다른 과제를 마주했다. 진짜... 이번에는 진짜를 마주한 기분이었다.
뭐가 나왔다고, 아주 험한 게
(feat. 파묘)
솔직히 처음 과제 주제를 봤을 때는 우습게 봤다. 그러다 큰코 다칠 줄도 모르고, 대차게 질 줄도 모르고... React-Native로 만들었던 때만 생각하고 쉽게 생각했던 것 같다. 전혀 다른데, 전혀 난이도 자체가 다른데 이런 멍청한 WOOD...
그제부터 쭉 과제만 쥐고 있느라 다른 거 하나 못 했다. 일단 부딪히고 보는 스타일인데도 불구하고 진짜 머리통이 터질 것 같았다. 그제는 심지어 12시간 + a로 붙들고 있느라고 마지막에는 코드 꼴도 보기 싫어졌더란다. 그 꼬부랑거리는 글씨를 보고 있으면 뇌까지 꼬불꼬불 해지는 것 같아서 더는 못하겠다고 두손 두발 다 들고 자러갔었다.
그리고 바로 어제, 다시 해보려고 코드를 보는데 진짜 아무래도 안 될 것 같아서 다시 새로 프로젝트를 만들어서 새로운 마음으로 시작했다. 처음과 같은 마음으로.✨ (는 책상에 머리 박고 싶었다.)
문제
→ ToDo를 추가할 때 제목을 공백으로 둘 수 있는 문제가 있었다.
guard let text = textfield.text, !text.isEmpty else {
return
}
guard
문을 사용하여 텍스트 필드에서 가져온 텍스트가 공백 여부 확인
→ 텍스트 필드에서 가져온 텍스트 공백 여부를 확인하고, 비어있을 경우 함수 종료
문제
→ ToDo를 간략하게 메모하는 쪽으로 구현하고 싶었기 때문에 글자 수를 제한하고자 했다.
if newLength > 16 {
showAlert(message: "텍스트는 16글자 이하로만 입력할 수 있습니다.")
return false // 입력 제한
}
// 생략
func showAlert(message: String) {
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "확인", style: .default, handler: nil))
present(alertController, animated: true, completion: nil)
}
if newLength > 16 { ... }
→ 입력된 텍스트가 16자를 초과하는지 확인하는 조건문
→ 만약 길이가 16자를 초과한다면 아래의 코드 블록 실행
showAlert(message: "텍스트는 16글자 이하로만 입력할 수 있습니다.")
→ 길이가 16자를 초과한 경우 사용자에게 "텍스트는 16글자 이하로만 입력할 수 있습니다."라는 메시지 출력
문제
→ cell
의 스위치를 토글하면 상태 값이 변경되어야 하지만 변경되지 않는 문제가 있었다.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 생략
let todo = todos[indexPath.row]
let mySwitch = UISwitch()
mySwitch.isOn = todo.isCompleted
mySwitch.tag = indexPath.row
mySwitch.addTarget(self, action: #selector(didChangeSwitch(_:)), for: .valueChanged)
cell.accessoryView = mySwitch
// 생략
}
// 생략
@objc func didChangeSwitch(_ sender: UISwitch) {
let index = sender.tag
todos[index].isCompleted = sender.isOn
table.reloadRows(at: [IndexPath(row: index, section: 0)], with: .none)
}
tableView(\_:cellForRowAt:)
→ 테이블 뷰의 각 셀을 구성하는 메서드
→ 각 셀에는 ToDo item의 정보가 표시되고, 오른쪽에는 완료 상태를 나타내는 스위치 추가
→ 스위치의 초기 상태는 해당 ToDo item의 완료 상태(isCompleted)에 따라 설정
→ 스위치 태그를 indexPath
의 행 번호로 설정,
값이 변경될 때마다 didChangeSwitch(\_:)
를 호출하도록 addTarget
메서드 사용
didChangeSwitch(\_:)
→ 스위치 값이 변경될 때 호출되는 콜백 메서드
→ 해당 인덱스를 찾아 해당하는 ToDo item의 완료 상태를 업데이트하고
테이블 뷰의 해당 cell
을 다시 로드하여 변경된 값 반영
문제
→ 새로운 ToDo를 추가할 때마다 아이디가 모두 1로 설정되어 추가되는 아이디들이 모두 중복되는 문제가 있었다.
@IBAction func saveButtonTapped(_ sender: UIBarButtonItem) {
guard let text = textfield.text, !text.isEmpty else {
return
}
var id = todos.count + 1
let todoItem = TodoItems(id: id, title: text, isCompleted: false)
delegate?.didSaveTodoItem(todoItem)
navigationController?.popViewController(animated: true)
}
guard let text = textfield.text, !text.isEmpty else { return }
→ 텍스트 필드에서 가져온 텍스트 공백 여부를 확인하고, 비어있을 경우 함수 종료
var id = todos.count + 1
→ todos
배열의 항목 개수에 1
을 더하여 새로운 할 일 항목의 아이디 설정
let todoItem = TodoItems(id: id, title: text, isCompleted: false)
→ 새로운 ToDo item 객체 생성
→ id는 위에 설정한 id
변수 사용, 사용자가 입력한 텍스트와 완료 상태는 각각 text
와 false
로 설정
delegate?.didSaveTodoItem(todoItem)
→ ToDo item을 저장하기 위해 delegate
를 통해 didSaveTodoItem
메서드 호출
→ ToDo item이 View Controller
로 전달되어 추가 됨
navigationController?.popViewController(animated: true)
→ 사용자가 ToDo item를 추가한 후 다시 이전 화면으로 이동
그러니까... 이렇게 하면 모든 게 다 해결 될 줄 알았다. 저장 버튼을 눌렀을 때 입력받은 텍스트 값과 함께 todos.count
를 사용해 배열에 포함된 아이템의 수를 헤아려 +1을 하려고 했으나 추가되지 않는 문제가 발생.
더불어,
Cannot find 'todos' in scope
해당 파일 내에 todos 가 를 찾을 수 없다는 에러까지 확인되었다.
분명 ViewController
에서 위 코드가 있는 파일인 AddViewController
로
struct TodoItems {
var id: Int
var title: String
var isCompleted: Bool
}
위 구조체로 접근할 수 있도록 코드를 작성했는데도 불구하고 해결되지 않았다. 그리고 여기서 위 코드와 관련된 문제가 더 있었다. 나는 각 cell
을 날짜를 title
로 한 section
으로 하나의 묶음으로 보여주고 싶었고, 그렇기 때문에 배열을 두 개로 나누어 만들었었다.
// 날짜 별 item 그룹화
var todosGroupedByDate = [Date: [TodoItems]]()
// item 배열
var todos = [TodoItems]()
그런데 이게 문제가 되었다. 처음 만들 때는 그룹화를 하지 않고 만들었기 때문에 데이터를 추가하거나 화면에 출력할 시 위와 같이 그룹화 된 값을 바인딩 하거나 사용하는 것이 아니라 하나의 배열에서 빼내어 사용하는 것이었고, 그룹화를 하려고 배열을 추가한 후부터는 그 방법으로 하면 안 되었기 때문에 코드 자체가 내부적으로 꼬였다. 꼬인 걸 하나씩 풀어내려 했으나 도저히 해결되지 않았다.
사실 이 문제가 새로 프로젝트를 만들게 된 가장 큰 이유가 되었다. 왜?
끝까지 해결하지 못했으니까...
다시는 새로 보고 싶지 않다...
그리고 다시 어제, 새로 구성한 프로젝트에서는 단계별로 차근차근 진행해보았다.
새로 만드는 김에 전에는 ToDo 추가 시 텍스트 필드를 사용했으나 과제조건에 따라 UIAlertController
를 사용했다.
Main Story Board
ToDo 추가 / 입력
@IBAction func iconButtonTapped(_ sender: UIButton) {
let alertController = UIAlertController(title: nil, message: "새로운 ToDo 추가", preferredStyle: .alert)
alertController.addTextField { textField in
textField.placeholder = "오늘의 ToDo를 입력해 주세요."
}
let addAction = UIAlertAction(title: "추가", style: .default) { _ in
if let textField = alertController.textFields?.first, let text = textField.text, !text.isEmpty {
let todoItem = TodoItem(title: text)
self.todos.append(todoItem)
self.table.reloadData()
} else {
print("텍스트가 입력되지 않았습니다.")
}
}
alertController.addAction(addAction)
let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
사용자에게 알림을 표시하고 대화 형식으로 상호작용할 수 있는 컨트롤러인
UIAlertController 사용하여 ToDo item 를 추가하도록 했다.
- UIAlertController
를 생성하여 새로운 ToDo를 추가할 때 사용자에게 입력 필드 제공
- 입력 필드에 내용을 입력하고 추가
버튼 클릭 시 입력된 내용으로 새로운 ToDo item 객체 생성
- 생성된 ToDo item 객체를 todos
배열에 추가
- 테이블 뷰 재로드
ToDo section / cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let date = Set(todos.map { $0.saveDate }).sorted()[indexPath.section]
let todosOnDate = todos.filter { $0.saveDate == date }
if expandedSections.contains(indexPath.section) {
let todoItem = todosOnDate[indexPath.row]
cell.textLabel?.text = todoItem.title
cell.textLabel?.textColor = todoItem.isCompleted ? .gray : .black
cell.selectionStyle = .none
var mySwitch: UISwitch
if let accessory = cell.accessoryView as? UISwitch {
mySwitch = accessory
} else {
mySwitch = UISwitch()
mySwitch.addTarget(self, action: #selector(didChangeSwitch(_:)), for: .valueChanged)
cell.accessoryView = mySwitch
}
mySwitch.tag = indexPath.row
mySwitch.isOn = todoItem.isCompleted
} else {
cell.textLabel?.text = ""
cell.accessoryView = nil
}
return cell
}
테이블 뷰의 각 셀이 해당하는 ToDo item을 표시하고 사용자가 UISwitch를 사용하여 완료 여부를 토글할 수 있도록 했다.
- dequeueReusableCell(withIdentifier:for:)
→ 재사용 가능한 cell
을 가져옴
- 해당 섹션의 날짜를 가져오고, 해당 날짜에 해당하는 ToDo item들을 필터링하여 가져옴
- 만약 섹션이 펼쳐져 있을 경우
→ 해당 indexPath
에 해당하는 ToDo item을 가져와 cell
에 표시
→ ToDo item의 완료 여부에 따라 텍스트 색상 변경
→ cell
의 accessoryView
로 UISwitch
설정, 이 UISwitch
는 ToDo item의 값을 변경할 수 있음
- 섹션이 펼쳐져 있지 않은 경우
→ cell
의 텍스트를 비움
→ cell
의 accessoryView
를 제거하여 cell
숨기기
→ 구성된 cell
반환
ToDo isCompleted - true / false
@objc func didChangeSwitch(_ sender: UISwitch) {
let indexPath = IndexPath(row: sender.tag, section: 0)
// 해당 indexPath의 todoItem 가져오기
// 생략
todoItem.isCompleted = sender.isOn
if let index = todos.firstIndex(where: { $0.id == todoItem.id }) {
todos[index] = todoItem
}
table.reloadRows(at: [indexPath], with: .automatic)
}
사용자가 UISwitch를 토글할 때마다 해당하는 ToDo item의 완료 상태를 업데이트하고,
테이블 뷰의 해당 cell을 다시 로드하여 변경 사항을 반영했다.
- UISwitch
의 tag
를 사용하여 해당하는 IndexPath
생성
- IndexPath
에 해당하는 ToDo item의 완료 상태를 UISwitch
의 값에 따라 업데이트
- 변경된 ToDo item을 배열에 반영하여 업데이트
ToDo cell delete
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let section = indexPath.section
// 섹션의 모든 아이템 가져오기
// 생략
todos.remove(at: indexPath.row)
let itemCountInSection = todosInSection.count
if itemCountInSection == 1 {
// 섹션의 아이템이 한 개인 경우 섹션 삭제
todos.removeAll { $0.saveDate == date }
tableView.deleteSections(IndexSet(integer: section), with: .automatic)
} else {
// 섹션의 아이템이 여러 개인 경우 아이템만 삭제
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
UITableView에서 행을 삭제할 때 해당하는 ToDo item을 삭제하고, 남아 있는 아이템의 개수에 따라 섹션 또는 행을 삭제하도록 했다.
- 삭제를 시도한 ToDo item의 섹션을 가져옴
- 섹션에 속한 모든 ToDo item을 가져옴
- 삭제를 시도한 행에 해당하는 ToDo item을 배열에서 제거
- 해당 섹션에 남아 있는 아이템의 개수 확인
- 남아 있는 아이템의 개수에 따른 삭제 동작
→ 아이템이 한 개인 경우: 해당 섹션을 삭제
→ 아이템이 여러 개인 경우: 해당 행만 삭제
'𝗔𝗦𝗦𝗜𝗚𝗡𝗠𝗘𝗡𝗧 > [내배캠] 4주차 - ToDo' 카테고리의 다른 글
[내일배움캠프] iOS 입문 과제 - My Todo List(02) (4) | 2024.03.22 |
---|