05.01 (목) iOS 사전 캠프

2025. 5. 1. 22:23·🖋️ TIL Journal
-->
TIL은 편한 말투로 작성됩니다.

 

📍 오늘 학습한 내용 정리
1. Todo 앱 데이터 삭제, 수정 기능 구현
2. Todo 앱 완료 체크 및 즐겨찾기 구현
3. 삭제 기능 구현하며 공부한 점

 


 

오늘은 iOS 강의를 보며 만들던 Todo앱에서 "데이터 삭제", "수정" 기능의 구현과, 할 일 "완료 체크", "즐겨찾기" 기능을 구현했다.

 

강의 내용이 길지 않다고 생각했는데, 강의를 멈춰두고 코드를 먼저 작성해보거나, 궁금한 부분을 공부하고 이해하는데 시간이 꽤나 걸렸다.. 미래의 나는 이런거 금방금방 할 수 있도록 열심히 해야지..

 

 

 

우선, 가장 오른쪽에 새로운 ViewController를 추가하여 수정 화면을 만들었고, 메인 화면에 있는 "할 일" 셀을 클릭하면 해당 내용이 TextField에 자동으로 채워져 수정할 수 있도록 구현했다.

 

작성한 코드들은 아래에 접은 글로 정리해두었다

 

더보기

ViewController (메인 코드)

import UIKit


struct Todo {
    var title: String
    var isDone: Bool
    var isFavorite: Bool
}

// UITableViewDataSource: "데이터를 제공하는 역할" (몇 개의 셀인지, 셀에 어떤 내용인지)
// UITableViewDelegate: "셀을 눌렀을 때, 셀의 높이, 액션 등 UI 동작을 관리" (didSelectRowAt 같은 것들)
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, AddTodoDelegate, EditTodoDelegate {
   
    
    @IBOutlet weak var tableView: UITableView!
    
    var todos: [Todo] = [
        Todo(title: "강의 보기", isDone: false, isFavorite: false),
        Todo(title: "운동하기", isDone: true, isFavorite: false),
        Todo(title: "빨래하기", isDone: false, isFavorite: false),
        Todo(title: "사진찍기", isDone: false, isFavorite: true),
        Todo(title: "롤하기", isDone: false, isFavorite: false)
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // UITableView는 스스로 데이터도 모르고, 셀도 생성하지 못함.
        // 그렇기에 "= self" 를 통해서 ViewController가 대신 담당해줌
        tableView.dataSource = self
        tableView.delegate = self
        
    }
    
    // 아래 2가지 tableView의 "numberOfRowsInSection", "cellForRowAt"은 UITableViewDataSource 프로토콜에 꼭 있어야 하는 규약이다.
    // numberOfRowsInSection: 하나의 섹션 안에 몇 개의 셀(row)이 있을지를 정하는 함수
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return todos.count
    }
    
    // cellForRowAt: 특정 indexPath(섹션, 행 위치)에 어떤 셀(Cell)을 표시할지 정하는 함수
    // 주어진 indexPath에 맞는 셀을 생성(dequeue)하거나 재사용하고, 셀의 내용을 설정해서 반환함
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let todo = todos[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "TodoCell") as! TodoTableViewCell
        cell.titleLabel.text = todo.title
        
        let doneImage = todo.isDone ? "checkmark.circle.fill" : "circle"
        let favImage = todo.isFavorite ? "star.fill" : "star"
        
        cell.doneButton.setImage(UIImage(systemName: doneImage), for: .normal)
        cell.favoriteButton.setImage(UIImage(systemName: favImage), for: .normal)
        
        cell.toggleDone =  { [weak self] in
            self?.todos[indexPath.row].isDone.toggle()
            self?.animateCell(cell)
        }
        
        cell.toggleFavorite = { [weak self] in
            self?.todos[indexPath.row].isFavorite.toggle()
            self?.animateCell(cell)
        }
        
        return cell
    }
    
    func animateCell(_ cell: UITableViewCell) {
        UIView.animate(withDuration: 0.2, animations: {
            cell.contentView.alpha = 0.5
        }) { _ in
            UIView.animate(withDuration: 0.2) {
                cell.contentView.alpha = 1.0
            }
        }
        tableView.reloadData()
    }
    
    
    // didSelectRowAt: 테이블 뷰에서 특정 셀(row)을 사용자가 터치했을 경우에 호출되는 함수
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // indexPath가 눌렸을 경우, 애니메이션을 넣어주는 코드 (false는 애니메이션 작동 x)
        tableView.deselectRow(at: indexPath, animated: true)
        
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let editVC = storyboard.instantiateViewController(withIdentifier: "EditTodoViewController") as! EditTodoViewController
        // 셀 클릭할때, index랑 원래 값이 무엇인지 editVC에 알려줌
        editVC.originText = todos[indexPath.row].title
        editVC.index = indexPath.row
        editVC.delegate = self
        
        
        
        present(editVC, animated: true)
    }
    
    
    // MARK: 데이터 삭제 기능
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            todos.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .automatic)
        }
        
    }

    
    // AddTodoViewController는 이 delegate를 통해 didAddNewTodo를 호출할 예정이기에 ViewController에서 이 함수 구현이 필요함
    // 또한 ViewController는 AddTodoDelegate 프로토콜을 채택했기에, 프로토콜이 요구하는 didAddNewTodo 함수를 반드시 구현해야함
    func didAddNewTodo(_ todo: String) {
        todos.append(Todo(title: todo, isDone: false, isFavorite: false))
        // 테이블뷰는 값을 리로드해서 갱신해줘야함
        tableView.reloadData()
    }
    
    func didEditTodo(text: String, index: Int) {
        todos[index].title = text
        tableView.reloadData()
    }
    
    
    @IBAction func didTapAddTodo(_ sender: Any) {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        // 스토리보드 안에 "AddTodoViewController"이라는 Storyboard ID를 가진 ViewController 인스턴스를 생성
        // as!를 통해 AddTodoViewController 타입으로 강제 형변환을 함. (내부의 delegate 같은 기능을 쓰기 위해서)
        // 만약 as!를 통해 형 변환을 안했다면, 아래의 addVC.delegate = self에서 에러가 발생함
        let addVC = storyboard.instantiateViewController(identifier: "AddTodoViewController") as! AddTodoViewController
        // AddTodoViewController의 delegate를 ViewController(self)로 지정하여, 할 일 추가 요청을 전달받을 수 있도록 설정
        addVC.delegate = self
        present(addVC, animated: true)
    }
}
더보기

AddTodoViewController

import UIKit

// protocol: 규칙의 목록. 이 규칙을 지키겠다고 하면 이 안에 함수를 반드시 구현해야함
// 할 일을 추가하는 방법은 모르기에, 그 일을 대신할 객체(ViewController)가 필요함
// 근데 그 객체 또한 어떤 함수를 정의해야 하는지 모르기에, 어떤 함수를 구현해야하는지 protocol을 통해 약속으로 알려주는 것
protocol AddTodoDelegate: AnyObject {
    // 그렇기에 ViewController에서 AddTodoDelegate를 사용할 때,
    // didAddNewTodo 함수를 정의하지 않으면 에러가 발생함
    func didAddNewTodo(_ todo: String)
}

class AddTodoViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!
    // 순환 참조가 일어나지 않도록 약한 참조를 사용
    weak var delegate: AddTodoDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
    
    @IBAction func didTapSave(_ sender: Any) {
        guard let text = textField.text,
                !text.isEmpty else {
            return
        }
        // 위에서 textField에 받은 text를 didAddNewTodo 함수를 호출하여 값을 넘겨줌
        delegate?.didAddNewTodo(text)
        // 화면 닫기
        dismiss(animated: true)
    }
}
더보기

TodoTableViewCell 

import UIKit

class TodoTableViewCell: UITableViewCell {

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var doneButton: UIButton!
    @IBOutlet weak var favoriteButton: UIButton!
    
    var toggleDone: (() -> Void)?
    var toggleFavorite: (() -> Void)?
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    @IBAction func didTapDone(_ sender: Any) {
        toggleDone?()
    }
    
    @IBAction func didTapFavorite(_ sender: Any) {
        toggleFavorite?()
    }
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}
더보기

EditTodoViewController 

import UIKit

protocol EditTodoDelegate: AnyObject {
    func didEditTodo (text: String, index: Int)
}

class EditTodoViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    
    var originText: String?
    var index: Int?
    weak var delegate: EditTodoDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        textField.text = originText

    }

    @IBAction func didTapUpdate(_ sender: Any) {
        
        guard let newText = textField.text, !newText.isEmpty,
              let index = index else { return }
        delegate?.didEditTodo(text: newText, index: index)
        dismiss(animated: true)
    }
}

아래는 시뮬레이터를 이용해 실행해본 영상이다.

 

 

실행 영상

 

 

 

 

🤔 공부하면서 생긴 궁금증

todo 앱에서 데이터를 삭제하는 기능을 구현할 때, 나는 tableView.reloadData()를 사용해서 todos에 값이 remove 되고, 그로 인해 바뀐 화면을 reload 하는 방식을 사용했다.

처음 작성한 코드

 

근데 이 다음에 강의를 재생하니, 아래 코드처럼 reloadData() 대신, tableView.deleteRows(at:with:)메서드를 사용하시는 걸 보았다.

이때, "어? 그냥 reloadData()를 써서 화면을 다시 그리면 되는거 아닌가? 코드도 더 길어지고 쓰기 복잡해 보이는데.." 싶어서 찾아보니, 겉으로 보기에는 비슷하게 동작하지만 내부에는 아래와 같은 차이점이 있다고 한다.

 

  • tableView.reloadData()
    • 전체 테이블 뷰를 리로드. (모든 셀을 다시 그린다.)
    • 성능 측면에서 덜 효율적일 수 있음 (데이터가 많을수록 영향이 클 것이다.)
    • 사용자 경험 측면에서는 부드러운 삭제 애니메이션이 없음

 

  • tableView.deleteRows(at:with:)
    • 지정된 셀을 삭제하고, 해당 셀만 UI에서 제거함
    • 사용자가 기대하는 삭제 애니메이션이 자연스럽게 나옴
    • 부분 업데이트이므로 성능도 더 좋고, 직관적임

 

 

확실히 코드를 작성하는 건 그냥 간단히 reloadData()를 하면 편하겠지만, 많~~은 데이터들 중에서 일부분만 다시 수정하면 되는데 굳이 전체 데이터를 다시 다 그리면서 성능도 저하되고, 애니메이션도 없어서 사용자 측면에서도 좋은 방법은 아닌 것 같았다.

 

실제로 둘 다 써보니, reloadData()는 그냥 애니메이션 없이 갑자기 사라지는 반면, deleteRows(at:with:)는 애니메이션도 넣어줘서 사용자 측면에서도 좋다는 걸 느꼈다.

 

간단한 화면이라 그런지 성능 차이를 느끼지 못했지만, 이건 나중에 더 많은 데이터를 다룰 때 사용해 보면 느낄 수 있지 않을까 싶다. 

 

 

 

그런데, "그러면 reloadData()는 애니메이션도 없고, 성능도 별로고, 언제 사용하는 거지??" 라는 궁금증이 생겼다.

 

보통은 아래의 경우에서 사용한다고 한다.

- 전체 데이터가 크게 바뀐 경우
- 데이터 수가 적고, 성능이 중요하지 않은 경우 (애니메이션도 상관 x)
- 여러 UI가 한 번에 갱신되어야 할 때

 

 

마지막으로 간단히 정리하면,

  • 전체 데이터가 교체가 되는데, 애니메이션도 상관없고 큰 데이터를 다루는것도 아니야! -> reloadData() 사용
  • 어느정도 데이터가 있는데 일부분만 삭제할거야! 애니메이션도 있음 좋아! -> deleteRows(at:with:) 사용

이렇게 본인의 상황에 따라 골라서 사용하면 될 것 같다.

 

 

+ 추가

TIL을 작성하다가 비교가 좀 잘못된 것 같아서 추가로 작성함.

 

reloadData()는 전체 데이터를 갱신하는 메서드이고, deleteRows(at:with:)는 특정 셀을 삭제할 때 사용하는 메서드이다.

 

여기서는 reloadData()를 삭제 예시로 들면서 둘을 비교하였지만, 일부 데이터만 삭제하지 않고 갱신하려는 목적이라면 reloadRows(at:with:)를 사용하는 것이 적절하다.

 

 

오늘은 여기까지... 아직 todo 앱을 껐다 켜면 사라지기에,
내일은 UserDefaults를 활용해 데이터를 저장하는 기능까지 구현하여 Todo 앱을 완성해볼 예정이다. 

 

 

'🖋️ TIL Journal' 카테고리의 다른 글

05.07 (수) iOS 사전 캠프  (0) 2025.05.07
05.02 (금) iOS 사전 캠프  (1) 2025.05.02
04.30 (수) iOS 사전 캠프  (0) 2025.04.30
04.29 (화) iOS 사전 캠프  (1) 2025.04.29
04.28 (월) iOS 사전 캠프  (1) 2025.04.28
'🖋️ TIL Journal' 카테고리의 다른 글
  • 05.07 (수) iOS 사전 캠프
  • 05.02 (금) iOS 사전 캠프
  • 04.30 (수) iOS 사전 캠프
  • 04.29 (화) iOS 사전 캠프
MoriOS
MoriOS
기억하기 위해 기록하는 공간 🖋️
  • MoriOS
    MoriOS
    MoriOS
  • 전체
    오늘
    어제
    • 분류 전체보기 (67)
      • 📌 Swift (12)
      • 📱 iOS (11)
      • 💡 Algorithm (1)
      • ❕Data structure (4)
      • 🪙 Python (0)
      • ⚙️ Git (3)
      • 🖋️ TIL Journal (33)
      • 📝 Etc (3)
  • 블로그 메뉴

    • GitHub
  • 인기 글

  • 태그

    TiL
    Codable
    제약조건 수정
    후행클로저
    커밋 아이콘
    remakeconstraints
    Optional
    스크롤 길이
    uikit
    cow 참조 타입
    프로그래머스
    swift
    커밋 이모지
    prepareconstraints
    swift optional
    weak
    셀 높이
    GitHub
    cow 쓰기 복사
    Components
    updateconstraints
    git moji
    제약조건 변경
    swift cow
    makeconstraint
    ios
    static
    Split
    SnapKit
    cow 값 타입
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
MoriOS
05.01 (목) iOS 사전 캠프
상단으로

티스토리툴바