테이블 뷰로 만드는 To Do List (Check List)
- 네비게이션 컨트롤러
- 알러트 창으로 할 일을 추가
- 셀 스와이프로 제거
- 수정 화면에서 셀 제거 및 순서 변경
- 앱을 종료 후 재실행 시 데이터 유지
완성 화면 ▼
▼ 참고할 만한 이전 글들
네비게이션 컨트롤러
[Swift/iOS프로그래밍] navigation controller 4가지 방법, 세그 vs 코드,push vs present
스위프트로 화면전환을 구현하는 방식 중에 네비게이션 컨트롤러가 있다. 세그로 구현하는 것과 코드로 구현하는 것이 있는데 이 중에서도 푸쉬와 프레젠트라는 방식이 있다. 오늘은 이런 4가
yejprogramming.tistory.com
뷰 간 데이터 전달
[Swift/iOS프로그래밍] 뷰와 뷰 사이 데이터 전달
지난 화면전환 네비게이션 글에서 실습한 프로젝트 이후 부분이니 참고바람 https://yejprogramming.tistory.com/62 [swift/iOS프로그래밍] navigation controller 4가지 방법, 세그 vs 코드,push vs present 스위..
yejprogramming.tistory.com
스토리보드 ▼
주석을 상세하게 달았으니 이를 통해 코드 참고 바람
Task 구조체
import Foundation
struct Task {
var title: String
var done: Bool
}
- 추가 버튼
기능 : 알러트창, 할 일 등록하기, 취소 버튼
@IBAction func addBtn(_ sender: UIBarButtonItem) {
let alert = UIAlertController(title: "할 일 추가", message: nil, preferredStyle: .alert) //알러트창 객체 생성
let registerButton = UIAlertAction(title: "등록", style: .default, handler: { [weak self] _ in //[weak self] 클로저 선언부에서 캡처목록을 정의하는 이유는 클로저는 클래스처럼 참조타입이라 self로 인스턴스를 캡처할때 강한 순환참조를 할 수 있는데 이렇게 되면 순환참조에 연관된 객체들은 레퍼런스 카운터가 0에 도달하지 않고 메모리 누수가 발생함! 즉, 클로저 선언부에서 캡처목록을 정의해야함
guard let title = alert.textFields?[0].text else { return } //등록버튼을 눌렀을 때 textfield의 값을 가져올 수 있음
let task = Task(title: title, done: false)
self?.tasks.append(task)
self?.tableView.reloadData() //테이블뷰에 값이 추가될 때마다 갱신
}) //handler 부분에는 버튼을 눌럿을 때 어떤 액션을 할 것인지를 정의함. 그래서 closure로 작성
let cancelButton = UIAlertAction(title: "취소", style: .cancel, handler: nil) //취소 버튼을 눌렀을 때는 별다른 액션이 없으니 handler은 nil
alert.addAction(cancelButton) //addAction은 알러트에 action 객체를 추가하는 메서드
alert.addAction(registerButton)
//alert에 text field를 추가하는 메서드, configurationHandler 파라미터는 알러트를 표시하기전 text field를 구성하기 위한 클로저, 반환값x, 단일매개변수
alert.addTextField(configurationHandler: { textField in
textField.placeholder = "할 일 을 입력해주세요."
})
self.present(alert, animated: true, completion: nil) //알러트창 띄우기
}
- 편집 버튼
@IBAction func editBtn(_ sender: UIBarButtonItem) {
//doneButtonTap 메서드를 생성하였으니 edit 버튼 누르면 편집모드로 들어가게끔 코드작성
guard !self.tasks.isEmpty else { return } //테이블이 비어있지 않을 때만 편집모드 작동
self.navigationItem.leftBarButtonItem = self.doneButton
self.tableView.setEditing(true, animated: true)
}
@objc func doneButtonTap() {
//done을 누르면 edit버튼으로 변경되고 편집모드 종료
self.navigationItem.leftBarButtonItem = self.editBtn
self.tableView.setEditing(false, animated: true)
}
- 할 일을 저장하고 로딩
func saveTasks() {
//할일 목록을 딕셔너리 배열 형태로 저장
let data = self.tasks.map {
[
"title": $0.title,
"done": $0.done
]
}
//UserDefaults는 사용자의 기본 데이터베이스를 기록하고 가져오는 인터페이스. 키-값 쌍임, 앱 전체에서 1개
let userDefaults = UserDefaults.standard
userDefaults.set(data, forKey: "tasks") //UserDefaults에 저장, value, key 쌍
}
func loadTasks() {
//저장된 데이터를 로드하는 함수
let userDefaults = UserDefaults.standard //UserDefaults에 접근
guard let data = userDefaults.object(forKey: "tasks") as? [[String: Any]] else { return } //forKey에는 데이터를 저장할 때 설정한 키값을 넣으면 됨, dictionary 형태로 타입 캐스팅을 함. 타입캐스팅 실패를 대비해 guard let
self.tasks = data.compactMap{
guard let title = $0["title"] as? String else { return nil }
guard let done = $0["done"] as? Bool else { return nil }
return Task(title: title, done: done) //return이 Task타입이 되도록 인스턴스화
}
}
전체 코드 ▼
//
// ViewController.swift
// ToDoList
//
// Created by 조은 on 2022/05/26.
//
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
@IBOutlet var editBtn: UIBarButtonItem!
var doneButton: UIBarButtonItem?
//tasks라는 변수를 생성하고 Tast 타입의 배열 초기화
var tasks = [Task]() {
//tasks 배열에 할일이 추가될때마다 userDefaults에 할일이 저장됨
didSet {
self.saveTasks()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTap))
self.tableView.dataSource = self
self.tableView.delegate = self
self.loadTasks() //저장된 할일을 불러옴
}
@objc func doneButtonTap() {
//done을 누르면 edit버튼으로 변경되고 편집모드 종료
self.navigationItem.leftBarButtonItem = self.editBtn
self.tableView.setEditing(false, animated: true)
}
@IBAction func editBtn(_ sender: UIBarButtonItem) {
//doneButtonTap 메서드를 생성하였으니 edit 버튼 누르면 편집모드로 들어가게끔 코드작성
guard !self.tasks.isEmpty else { return } //테이블이 비어있지 않을 때만 편집모드 작동
self.navigationItem.leftBarButtonItem = self.doneButton
self.tableView.setEditing(true, animated: true)
}
@IBAction func addBtn(_ sender: UIBarButtonItem) {
let alert = UIAlertController(title: "할 일 추가", message: nil, preferredStyle: .alert) //알러트창 객체 생성
let registerButton = UIAlertAction(title: "등록", style: .default, handler: { [weak self] _ in //[weak self] 클로저 선언부에서 캡처목록을 정의하는 이유는 클로저는 클래스처럼 참조타입이라 self로 인스턴스를 캡처할때 강한 순환참조를 할 수 있는데 이렇게 되면 순환참조에 연관된 객체들은 레퍼런스 카운터가 0에 도달하지 않고 메모리 누수가 발생함! 즉, 클로저 선언부에서 캡처목록을 정의해야함
guard let title = alert.textFields?[0].text else { return } //등록버튼을 눌렀을 때 textfield의 값을 가져올 수 있음
let task = Task(title: title, done: false)
self?.tasks.append(task)
self?.tableView.reloadData() //테이블뷰에 값이 추가될 때마다 갱신
}) //handler 부분에는 버튼을 눌럿을 때 어떤 액션을 할 것인지를 정의함. 그래서 closure로 작성
let cancelButton = UIAlertAction(title: "취소", style: .cancel, handler: nil) //취소 버튼을 눌렀을 때는 별다른 액션이 없으니 handler은 nil
alert.addAction(cancelButton) //addAction은 알러트에 action 객체를 추가하는 메서드
alert.addAction(registerButton)
//alert에 text field를 추가하는 메서드, configurationHandler 파라미터는 알러트를 표시하기전 text field를 구성하기 위한 클로저, 반환값x, 단일매개변수
alert.addTextField(configurationHandler: { textField in
textField.placeholder = "할 일 을 입력해주세요."
})
self.present(alert, animated: true, completion: nil) //알러트창 띄우기
}
func saveTasks() {
//할일 목록을 딕셔너리 배열 형태로 저장
let data = self.tasks.map {
[
"title": $0.title,
"done": $0.done
]
}
//UserDefaults는 사용자의 기본 데이터베이스를 기록하고 가져오는 인터페이스. 키-값 쌍임, 앱 전체에서 1개
let userDefaults = UserDefaults.standard
userDefaults.set(data, forKey: "tasks") //UserDefaults에 저장, value, key 쌍
}
func loadTasks() {
//저장된 데이터를 로드하는 함수
let userDefaults = UserDefaults.standard //UserDefaults에 접근
guard let data = userDefaults.object(forKey: "tasks") as? [[String: Any]] else { return } //forKey에는 데이터를 저장할 때 설정한 키값을 넣으면 됨, dictionary 형태로 타입 캐스팅을 함. 타입캐스팅 실패를 대비해 guard let
self.tasks = data.compactMap{
guard let title = $0["title"] as? String else { return nil }
guard let done = $0["done"] as? Bool else { return nil }
return Task(title: title, done: done) //return이 Task타입이 되도록 인스턴스화
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tasks.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) //cellForRowAt 메서드 파라미터인 indexPath
let task = self.tasks[indexPath.row]
cell.textLabel?.text = task.title
//체크마크 설정
if task.done {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
//편집모드에서 삭제버튼을 눌렀을때 어떤 셀이 선택되었는지를 알려주는 메서드
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt IndexPath: IndexPath){
self.tasks.remove(at: IndexPath.row)
tableView.deleteRows(at: [IndexPath], with: .automatic) //편집모드에서 삭제된 셀이 테이블뷰에서도 삭제, 편집모드에 안들어가도 swipe로 셀을 삭제할수있게함
if self.tasks.isEmpty {
self.doneButtonTap() //모든 셀이 삭제되면 편집 모드에서 빠져나감
}
}
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
//스와이프로 셀 순서 정렬
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
//행이 다른 위치로 이동하면 sourceIndexPath를 통해 원래 어디였는지, destinationIndexPath로어디로 이동했는지를 알려줌
var tasks = self.tasks //배열을 가져옴
let task = tasks[sourceIndexPath.row] //배열의 요소에 접근
tasks.remove(at: sourceIndexPath.row)
tasks.insert(task, at: destinationIndexPath.row)
self.tasks = tasks
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
var task = self.tasks[indexPath.row] //할일 배열에 접근
task.done = !task.done //truerㅏ 저장되어 있으면 false, flase가 저장되어 있으면 true
self.tasks[indexPath.row] = task
self.tableView.reloadRows(at: [indexPath], with: .automatic) //선택된 셀만 리로드
}
}
- 패스트 캠퍼스 강의 참고
'iOS > app' 카테고리의 다른 글
[iOS] 스위프트로 간단한 앱 만들기 :: 오늘 뭐 먹지? (2) | 2022.02.09 |
---|