iOS/app

[Swift/iOS] 간단한 To Do List 앱

Sweetft 2022. 4. 17. 21:59

테이블 뷰로 만드는 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