본문 바로가기

𝗔𝗦𝗦𝗜𝗚𝗡𝗠𝗘𝗡𝗧/[내배캠] 4주차 - ToDo

[내일배움캠프] iOS 입문 과제 - My Todo List(01) | 뭐가 나왔다고, 아주 험한 게(feat. 파묘)

 

 

 

 

 

My Todo List

 

 

 

 

새로운 주차가 시작 됐고, 또 다른 과제를 마주했다. 진짜... 이번에는 진짜를 마주한 기분이었다.

 

 


 

 

뭐가 나왔다고, 아주 험한 게

(feat. 파묘)

 

 

 

 

솔직히 처음 과제 주제를 봤을 때는 우습게 봤다. 그러다 큰코 다칠 줄도 모르고, 대차게 질 줄도 모르고... React-Native로 만들었던 때만 생각하고 쉽게 생각했던 것 같다. 전혀 다른데, 전혀 난이도 자체가 다른데 이런 멍청한 WOOD...

 

 

그제부터 쭉 과제만 쥐고 있느라 다른 거 하나 못 했다. 일단 부딪히고 보는 스타일인데도 불구하고 진짜 머리통이 터질 것 같았다. 그제는 심지어 12시간 + a로 붙들고 있느라고 마지막에는 코드 꼴도 보기 싫어졌더란다. 그 꼬부랑거리는 글씨를 보고 있으면 뇌까지 꼬불꼬불 해지는 것 같아서 더는 못하겠다고 두손 두발 다 들고 자러갔었다.

 

 

그리고 바로 어제, 다시 해보려고 코드를 보는데 진짜 아무래도 안 될 것 같아서 다시 새로 프로젝트를 만들어서 새로운 마음으로 시작했다. 처음과 같은 마음으로.✨ (는 책상에 머리 박고 싶었다.)

 

 

 


 

 

파일을 새로 만들기 전까지 진행했던 부분에 대해 정리해보면,




ToDo 입력 시 공백 여부

 

 문제 

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 item들의 아이디 중복

 

 문제 

새로운 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 변수 사용, 사용자가 입력한 텍스트와 완료 상태는 각각 textfalse로 설정

 

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]()

 

 

그런데 이게 문제가 되었다. 처음 만들 때는 그룹화를 하지 않고 만들었기 때문에 데이터를 추가하거나 화면에 출력할 시 위와 같이 그룹화 된 값을 바인딩 하거나 사용하는 것이 아니라 하나의 배열에서 빼내어 사용하는 것이었고, 그룹화를 하려고 배열을 추가한 후부터는 그 방법으로 하면 안 되었기 때문에 코드 자체가 내부적으로 꼬였다. 꼬인 걸 하나씩 풀어내려 했으나 도저히 해결되지 않았다.

 

 

사실 이 문제가 새로 프로젝트를 만들게 된 가장 큰 이유가 되었다. 왜?

 

 

 

끝까지 해결하지 못했으니까...

출처: pinterest

 

 


 

 

 

새로 만난 ToDo

다시는 새로 보고 싶지 않다...

 

 

 

 

그리고 다시 어제, 새로 구성한 프로젝트에서는 단계별로 차근차근 진행해보았다.

새로 만드는 김에 전에는 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의 완료 여부에 따라 텍스트 색상 변경

    cellaccessoryViewUISwitch 설정, 이 UISwitch는 ToDo item의 값을 변경할 수 있음

- 섹션이 펼쳐져 있지 않은 경우

    cell의 텍스트를 비움

    cellaccessoryView를 제거하여 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을 다시 로드하여 변경 사항을 반영했다.

 

- UISwitchtag를 사용하여 해당하는 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을 배열에서 제거

- 해당 섹션에 남아 있는 아이템의 개수 확인

- 남아 있는 아이템의 개수에 따른 삭제 동작

아이템이 한 개인 경우: 해당 섹션을 삭제

아이템이 여러 개인 경우: 해당 행만 삭제

 

 

 

 

Recent Posts
Visits
Today
Yesterday
Archives
Calendar
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31