꾸준히 안타치기

Todolist- App를 만들며 알게 된 것들 본문

iOS/Basic Study

Todolist- App를 만들며 알게 된 것들

글자줍기 2022. 9. 9. 02:17
반응형

https://www.youtube.com/watch?v=dPdVZOu1PrQ&list=PLyaXY4XhjFEki8z4ORj4wwGAQcDameiyX&index=18 

 

 

 

 

https://velog.io/@swiftist9891/ToyProject-ToDoList%ED%95%A0-%EC%9D%BC-%EB%93%B1%EB%A1%9D

 

[ToyProject] ToDoList(할 일 등록)

🍎 To Do List 🍏 기능 상세 TableView에 할 일들을 추가할 수 있습니다. 할 일을 삭제할 수 있습니다. 할 일의 우선순위를 재정렬할 수 있습니다. 할 일들을 데이터 저장소에 저장을 하여 앱을 재실

velog.io

✅ 기본기능 / 위 블로그를 참고해 TodoList App을 만들어보았다. 그대로 사용하지 않고 추가,변경후 알게된 것들 메모

✅ 추가한 기능

  •  글 수정기능
  • 체크On/OFF 추가
  • 마지막으로 작성한 글이 맨위로 오도록 했다.

탭바에 네비게이션컨트롤러 - 뷰컨이 연결되어있는 상태

뷰컨에 barbuttonitem으로 item과 + 버튼을 추가해준다. 하단은 체크리스트가 담길 테이블뷰

 

전체코드

더보기

ViewController.swift 

import UIKit

class ViewController: UIViewController {
    
    lazy var likes: [Int:Int] = [:]
    
    var tasks = [Task](){
        didSet{ // 프로퍼티 옵저버, tasks 배열에 할일이 추가될 때마다 유저 디폴트에 할일이 저장됨
            self.saveTask()
        }
    }
    
    var num = [Int]()
    var work = ""
    var state = ""
    
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet var barBtnEdit: UIBarButtonItem! //weak지우니까 됨..!!!!
    var doneButton: UIBarButtonItem?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTap))
        
        
        let nib = UINib(nibName:"TableViewCell",  bundle: nil)
        tableView.register(nib, forCellReuseIdentifier: "TableViewCell")
        tableView.dataSource = self
        tableView.delegate = self
        self.title = "할일"
        loadTasks()
        
        print(likes)
        
        
    }// 뷰디드끝
    
    override func viewWillAppear(_ animated: Bool) {
        loadTasks()
    }
    
    
    // done액션
    @objc func doneButtonTap() {
        self.navigationItem.leftBarButtonItem = self.barBtnEdit
        self.tableView.setEditing(false, animated: true) //done버튼 누르면 edit에서 빠져나오도록 함.
        print("done버튼누름")
        
    }
    // 편집버튼
    @IBAction func batBtnEditAction(_ sender: UIBarButtonItem) {
        guard !self.tasks.isEmpty else { return }
        self.navigationItem.leftBarButtonItem = self.doneButton
        self.tableView.setEditing(true, animated: true)
        print("편집버튼누름")
    }
    
    
    // 저장하기
    @objc func saveTask() {
        // 저장하기
        let data = self.tasks.map {
            [
                "title": $0.title,
                "detail": $0.detail,
                "done": $0.done
            ]
        }
        let userDefaults = UserDefaults.standard
        userDefaults.set(data, forKey: "tasks")
        userDefaults.synchronize()
    }
    
    
    
    //저장값 가져오기
    func loadTasks(){
        let userDefaults = UserDefaults.standard
        guard let data = userDefaults.object(forKey: "tasks") as? [[String: Any]] else { return }
        
        self.tasks = data.compactMap{
            guard let title = $0["title"] as? String else { return nil }
            guard let detail = $0["detail"] as? String else { return nil }
            guard let done = $0["done"] as? Bool else { return nil }
            return Task(title: title,detail: detail, done: done)
        }
        //.sorted { $0.title > $1.title } // 역순정렬
        tableView.reloadData()
        
        print("loadTasks" ,tasks)
    }
    
    
    
    //+버튼(팝업생성)
    @IBAction func btnAdd() {
        
        let alert = UIAlertController(title: "할 일 등록", message: "할 일을 입력해주세요.", preferredStyle: .alert)
        let registerButton = UIAlertAction(title: "등록", style: .default, handler: { [weak self] _ in
            guard let title = alert.textFields?[0].text else { return }
            guard let detail = alert.textFields?[1].text else { return }
            // 제목, 상세 넣기
            let task = Task(title: title, detail: detail,done: false)
//            self?.tasks.append(task)//뒤에넣기
            self?.tasks.insert(task , at: 0)//맨앞에 넣기
//            self?.tasks.append(task)
            self?.tableView.reloadData() // add된 할일들을 테이블뷰에 새로새로 업로드해주는 것
        })
        let cancelButton = UIAlertAction(title: "취소", style: .cancel, handler: nil)
        alert.addAction(cancelButton)
        alert.addAction(registerButton)
        alert.addTextField(configurationHandler: { textField in
            textField.placeholder = "할 일을 입력해주세요." })
        alert.addTextField(configurationHandler: { textField in
            textField.placeholder = "상세노트" })
        self.present(alert, animated: true, completion: nil)
    }
    
}



extension ViewController: UITableViewDelegate,UITableViewDataSource{
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.tasks.count
    }
    
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        
        var task = self.tasks[indexPath.row]
        //        task.done = !task.done   // 반대가 되게해줌
        self.tableView.reloadRows(at: [indexPath], with: .automatic)
        num = [indexPath.row]
        print(num)
        
        let alert = UIAlertController(title: "할 일 등록", message: "수정하기", preferredStyle: .alert)
        let registerButton = UIAlertAction(title: "등록", style: .default, handler: { [weak self] _ in
            guard let title = alert.textFields?[0].text else { return }
            guard let detail = alert.textFields?[1].text else { return}
            
            self?.num = [indexPath.row]
            let task = Task(title: title, detail: detail, done: task.done)
            // 그위치에 덮어쓰기위해 삭제후,insert
            self?.tasks.remove(at: self?.num[0] ?? 0)
            self?.tasks.insert(task , at: self?.num[0] ?? 0)
            // self?.tasks.append(task ,at: self?.num[0])// 뒤에추가
            self?.tableView.reloadData() // add된 할일들을 테이블뷰에 새로새로 업로드해주는 것
        })
        let cancelButton = UIAlertAction(title: "취소", style: .cancel, handler: nil)
        alert.addAction(cancelButton)
        alert.addAction(registerButton)
        alert.addTextField(configurationHandler: { textField in
            textField.text = self.tasks[indexPath.row].title })
        alert.addTextField(configurationHandler: { textField in
            textField.text = self.tasks[indexPath.row].detail })
        self.present(alert, animated: true, completion: nil)
    }
    
    
    // 쏄
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell") as? TableViewCell else {
            return UITableViewCell()
        }
        
        cell.delegate = self
        cell.index = indexPath.row
        
        let task = self.tasks[indexPath.row]
        cell.label?.text = task.title
        cell.labelDetail?.text = task.detail
        
        //task의 done이 true이면 눌러져있고 다시누를때의 상태
        if task.done{
            likes[indexPath.row] = 1
            cell.isTouched = false //하트눌림

        }else{
            likes[indexPath.row] = 0
            cell.isTouched = true //하트안눌림
        }
        

//         안눌러져있는데 누르는상태
                if likes[indexPath.row] == 1 {
                    cell.isTouched = true
                }else{
                    cell.isTouched = false
                }
        return cell
    }
    
    // commit for row at 이라는 메서드 구현
    // 삭제버튼 눌렀을때, 삭제버튼이 눌린 셀이 어떤 셀인지 알려주는 메서드
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        self.tasks.remove(at: indexPath.row) // remove cell 알려주는 것.
        tableView.deleteRows(at: [indexPath], with: .automatic)
        //automatic에니메이션을 설정하게 되면, 삭제버튼을 눌러서 삭제도 가능하고,
        // 우리가 평소에 사용하던 스와이프해서 삭제하는 기능도 사용 가능하다.
        if self.tasks.isEmpty { //모든셀이 삭제되면
            self.doneButtonTap() // done버튼 메서드를 호출해서 편집모드를 빠져나오게 구현.
        }
    }
    
    // 할일의 순서를 바꿀 수 있는 기능 구현
    // move row at 메서드를 구현 : 행이 다른 위치로 변경되면, souceIndexPath 파라미터를 통해 어디에 있었는지 알려주고, destinationIndexPath 파라미터를 통해 어디로 이동했는지 알려준다.
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        // talbe뷰 셀이 재정렬 되면, 할일을 저장하는 배열도 재정렬 되어야함.
        // 따라서 테이블뷰 셀이 재정렬된 순서대로, tasks 배열도 재정렬 해줘야해서 아래 처럼 구현
        var tasks = self.tasks
        let task = tasks[sourceIndexPath.row]
        tasks.remove(at: sourceIndexPath.row)
        tasks.insert(task, at: destinationIndexPath.row)
        self.tasks = tasks
    }
    
}

//셀클릭이벤트가 눌릴때 userDefault저장하기
extension ViewController: TestCellDelegate{
    func didPressCircle(for index: Int, like: Bool) {
        
        let task = self.tasks[index]
        
        //눌러져있을때  // 눌려있다면(1)-> 다시눌렀을때 0
        if likes[index] == 1{
//            likes[index] = 0
           
            let task2 = Task(title: task.title, detail: task.detail, done: false)
            self.tasks.remove(at: index)
            self.tasks.insert(task2, at: index)
            self.tableView.reloadData()
//            print("\(tasks)")
            print( likes[index], "1", task2.done)
            
        }else{
            // 안눌려있다면(0)-> 1로 다시
//            likes[index] = 1
            let task2 = Task(title: task.title, detail: task.detail, done: true)
            self.tasks.remove(at: index)
            self.tasks.insert(task2, at: index)
            self.tableView.reloadData()
            print( likes[index] , "2" , task2.done)
        }
//
            // 하트On
            if like{
                // 안눌려있으니까(0) -> 1로 만들어주기
                likes[index] = 1
                let task2 = Task(title: task.title, detail: task.detail, done: true)
                self.tasks.remove(at: index)
                self.tasks.insert(task2, at: index)
//                print("\(tasks)")
                print( likes[index] , "3" ,task2.done)
                self.tableView.reloadData()
               
            }
        
    }
    
}

TableViewCell.swift

import UIKit

protocol TestCellDelegate {
    func didPressCircle(for index: Int,like: Bool)
}

class TableViewCell: UITableViewCell {
    
    var delegate: TestCellDelegate?
    var index: Int?
    
    @IBOutlet weak var circle: UIButton!

    @IBOutlet weak var label: UILabel!
    
    @IBOutlet weak var labelDetail: UILabel!
    
    
    //On/Off
    @IBAction func circleAction(_ sender: UIButton) {
        guard let idx = index else {return}
             if sender.isSelected {
                 isTouched = true
                 delegate?.didPressCircle(for: idx, like: true)
             }else {
                 isTouched = false
                 delegate?.didPressCircle(for: idx, like: false)
             }
             sender.isSelected = !sender.isSelected
    }
    
    
    var isTouched: Bool? {
        didSet{
            if isTouched == true{
                circle.setImage(UIImage(systemName: "checkmark.circle.fill", withConfiguration:UIImage.SymbolConfiguration(scale:.large)),for: .normal)
            }else{
                circle.setImage(UIImage(systemName: "circle", withConfiguration:UIImage.SymbolConfiguration(scale:.large)),for: .normal)
            }
        }
    }
    
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
}

 

TableViewCell.xib

Task.swift

import Foundation

struct Task {
    var title: String // 할일 내용 저장
    var detail :String
    var done: Bool // 할일이 완료 되었는지 여부 저장
}

 

✅ didSet 프로퍼티 옵저버

class ViewController: UIViewController {
    
    lazy var likes: [Int:Int] = [:]
    
    var tasks = [Task](){
        didSet{ // 프로퍼티 옵저버, tasks 배열에 할일이 추가될 때마다 유저 디폴트에 할일이 저장됨
                // 프로퍼티의 값이 변경된 후에 실행할 내용
            self.saveTask()
        }
    }
    
   // 저장하기
    @objc func saveTask() {
        // 저장하기
        let data = self.tasks.map {
            [
                "title": $0.title,
                "detail": $0.detail,
                "done": $0.done
            ]
        }
        let userDefaults = UserDefaults.standard
        userDefaults.set(data, forKey: "tasks")
        userDefaults.synchronize()
    }

저장하기 버튼을 누르면 task배열에 title과 detail이 추가 되고, 추가 될때마다 userDefault에 tasks란 이름으로 저장이 된다.

map을 통해 title,detail,done 3개의 값을 저장함.

didSet은 프로퍼티 옵저버이다. 프로퍼티옵저버에는 willSet과 didSet이 있다.

옵저버는 특정프로퍼티를 관찰하고 있다가 값이 변경되면 호출된다.

  • willSet은 프로퍼티의 값이 변경되기 직전에 호출되는 옵저버
  • didSet은 프로퍼티의 값이 변경된 직후에 호출되는 옵저버 

https://bluedogs.tistory.com/482?category=1007876 

 

didSet 프로퍼티옵저버

https://silver-g-0114.tistory.com/107 [Swift] Property Observer 의 didSet, willSet 사용하기 Property Observer 프로퍼티 옵저버는 프로퍼티의 값의 변화를 관찰하고, 이에 응답합니다. 새로운 값이 프로퍼티..

bluedogs.tistory.com

 userDefault값 불러오기 / 정렬, 역순정렬

  //저장값 가져오기
    func loadTasks(){
        let userDefaults = UserDefaults.standard
        guard let data = userDefaults.object(forKey: "tasks") as? [[String: Any]] else { return }
        
        self.tasks = data.compactMap{
            guard let title = $0["title"] as? String else { return nil }
            guard let detail = $0["detail"] as? String else { return nil }
            guard let done = $0["done"] as? Bool else { return nil }
            return Task(title: title,detail: detail, done: done)
        }
        tableView.reloadData()
    }

userDefault 의tasks에 저장한 값을 가져올때 compactMap을 사용해 여러개를 불러올수 있다.

이때 역순정렬하고 싶으면  아래처럼 .sorted { $0.title > $1.title } 을 적용해주면된다. 마지막으로 추가한 값이 맨위에 오게된다.

  //저장값 가져오기
    func loadTasks(){
        let userDefaults = UserDefaults.standard
        guard let data = userDefaults.object(forKey: "tasks") as? [[String: Any]] else { return }
        
        self.tasks = data.compactMap{
            guard let title = $0["title"] as? String else { return nil }
            guard let detail = $0["detail"] as? String else { return nil }
            guard let done = $0["done"] as? Bool else { return nil }
            return Task(title: title,detail: detail, done: done)
        }
        .sorted { $0.title > $1.title } // 역순정렬
        tableView.reloadData()
    }

 

+ 버튼을 눌러 데이터 추가하기

 self?.tasks.append(task)// 리스트 뒤에넣기 
 self?.tasks.insert(task , at: 0)// 리스트 맨앞에 넣기(위치를 정해줄수 있다. 맨앞이므로 0번째)

    //+버튼(팝업생성)
    @IBAction func btnAdd() {
        
        let alert = UIAlertController(title: "할 일 등록", message: "할 일을 입력해주세요.", preferredStyle: .alert)
        let registerButton = UIAlertAction(title: "등록", style: .default, handler: { [weak self] _ in
            guard let title = alert.textFields?[0].text else { return }
            guard let detail = alert.textFields?[1].text else { return }
            // 제목, 상세 넣기
            let task = Task(title: title, detail: detail,done: false)
//            self?.tasks.append(task)//뒤에넣기
            self?.tasks.insert(task , at: 0)//맨앞에 넣기???
            self?.tableView.reloadData()
        })
        let cancelButton = UIAlertAction(title: "취소", style: .cancel, handler: nil)
        alert.addAction(cancelButton)
        alert.addAction(registerButton)
        alert.addTextField(configurationHandler: { textField in
            textField.placeholder = "할 일을 입력해주세요." })
        alert.addTextField(configurationHandler: { textField in
            textField.placeholder = "상세노트" })
        self.present(alert, animated: true, completion: nil)
    }

 

✅  테이블뷰 셀을 눌러 수정하기

수정할때는 가져온 위치에 덮어써야하기 때문에

가져온리스트 번호의 데이터를 remove 하고 그 자리에  insert해준다. 그리고 테이블뷰를 리로드해준다.

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)

        var task = self.tasks[indexPath.row]
        self.tableView.reloadRows(at: [indexPath], with: .automatic)
        num = [indexPath.row]
        
        let alert = UIAlertController(title: "할 일 등록", message: "수정하기", preferredStyle: .alert)
        let registerButton = UIAlertAction(title: "등록", style: .default, handler: { [weak self] _ in
            guard let title = alert.textFields?[0].text else { return }
            guard let detail = alert.textFields?[1].text else { return}
            
            self?.num = [indexPath.row]
            let task = Task(title: title, detail: detail, done: task.done)
            // 그위치에 덮어쓰기위해 삭제후,insert
            self?.tasks.remove(at: self?.num[0] ?? 0)
            self?.tasks.insert(task , at: self?.num[0] ?? 0)
            //self?.tasks.append(task ,at: self?.num[0])// 뒤에추가
            self?.tableView.reloadData() 
        })
        let cancelButton = UIAlertAction(title: "취소", style: .cancel, handler: nil)
        alert.addAction(cancelButton)
        alert.addAction(registerButton)
        alert.addTextField(configurationHandler: { textField in
            textField.text = self.tasks[indexPath.row].title })
        alert.addTextField(configurationHandler: { textField in
            textField.text = self.tasks[indexPath.row].detail })
        self.present(alert, animated: true, completion: nil)
    }

 

✅ commit for Row at, moveRowAt ,canMoveRowat

삭제버튼을 눌러서 일반삭제와, 스와이프 삭제 , 눌러서 이동하기 기능 

    // commit for row at
    // 삭제버튼 눌렀을때, 삭제버튼이 눌린 셀이 어떤 셀인지 알려주는 메서드
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        self.tasks.remove(at: indexPath.row) // remove cell 알려주는 것.
        tableView.deleteRows(at: [indexPath], with: .automatic)
        //automatic에니메이션을 설정하게 되면, 삭제버튼을 눌러서 삭제,스와이프삭제가능
        if self.tasks.isEmpty { //모든셀이 삭제되면
            self.doneButtonTap() // done버튼 메서드를 호출해서 편집모드를 빠져나오게 구현.
        }
    }
    
    // 할일의 순서를 바꿀 수 있는 기능 구현
    // move row at 메서드를 구현 : 행이 다른 위치로 변경되면, souceIndexPath 파라미터를 통해 어디에 있었는지 알려주고, destinationIndexPath 파라미터를 통해 어디로 이동했는지 알려준다.
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        // talbe뷰 셀이 재정렬 되면, 할일을 저장하는 배열도 재정렬 되어야함.
        // 따라서 테이블뷰 셀이 재정렬된 순서대로, tasks 배열도 재정렬 해줘야해서 아래 처럼 구현
        var tasks = self.tasks
        let task = tasks[sourceIndexPath.row]
        tasks.remove(at: sourceIndexPath.row)
        tasks.insert(task, at: destinationIndexPath.row)
        self.tasks = tasks
    }
}

테이블뷰의 셀이 재정렬되면, 할일을 저장한는 배열도 재정렬 되어야 하므로

기존 배열을 가져와서 remove하고 insert해준다.

 

weak

그리고 바버튼아이템의 Edit을 누르면 편집모드가 되고 Done으로 바뀐다. 이때 Done이 보이지 않는 현상이 있었는데

최초에 barBtunEdit버튼을 생성할때 그냥생성하면 weak가 붙어서 생성되는데 weak로 하면 메모리에서 해지되게 된다.

weak를 제거하니 사라지지 않았다. weak,strong 부분 공부필요함

   @IBOutlet weak var tableView: UITableView!
   @IBOutlet var barBtnEdit: UIBarButtonItem! //⭐️weak 
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTap))
   }// 뷰디드끝
   
   
    // done액션
    @objc func doneButtonTap() {
        self.navigationItem.leftBarButtonItem = self.barBtnEdit
        self.tableView.setEditing(false, animated: true) //done버튼 누르면 edit에서 빠져나오도록 함.
        print("done버튼누름")
        
    }
    // Edit버튼
    @IBAction func batBtnEditAction(_ sender: UIBarButtonItem) {
        guard !self.tasks.isEmpty else { return }
        self.navigationItem.leftBarButtonItem = self.doneButton
        self.tableView.setEditing(true, animated: true)
        print("편집버튼누름")
    }

 


 

반응형

'iOS > Basic Study' 카테고리의 다른 글

탭맨 라이브러리 사용하기  (0) 2022.09.11
AVFoundation 배경음악 넣기 / 타이머 만들기  (0) 2022.09.09
Dispatch Queue / Thread  (0) 2022.08.26
didSet 프로퍼티옵저버  (0) 2022.08.23
userDefault / 저장  (0) 2022.08.11
Comments