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.02 (금) iOS 사전 캠프 (0) | 2025.05.02 |
---|---|
04.30 (수) iOS 사전 캠프 (0) | 2025.04.30 |
04.29 (화) iOS 사전 캠프 (1) | 2025.04.29 |
04.28 (월) iOS 사전 캠프 (1) | 2025.04.28 |