(iOS)일기 앱 만들기

145206 단어 swiftiOSiOS

기능 상세

  • 일기장 탭을 누르면 일기 리스트 표시
  • 즐겨찾기 탭을 누르면 즐겨찾기한 일기 리스트 표시
  • 일기 등록, 수정, 삭제, 즐겨찾기 기능 구현

활용 기술

  • UITabBarController
  • UICollectionView
  • NotificationCenter

UITabBarController

UITabBar

앱에서 서로 다른 하위 작업, 뷰, 모드 사이의 선택을 할 수 있도록 탭바에 하나 혹은 하나 이상의 버튼을 보여주는 컨트롤

UITabBarController

다중 선택 인터페이스를 관리하는 뷰 컨트롤러
선택에 따라 어떤 자식 뷰 컨트롤러를 보여줄 것인지 결정

UICollectionView

데이터 항목의 정렬된 컬렉션을 관리하고 커스텀한 레이아웃을 사용해 표시하는 객체

구성 요소

Cell

컬렉션 뷰의 컨텐츠 표시

Supplementary View(필수 X)

섹션에 대한 정보 표시

Decoration View

컬렉션 뷰에 대한 배경을 꾸밀 때 사용

UICollectionViewFlowLayout

  1. Flow 레이아웃 객체를 작성하고 컬렉션 뷰에 이를 할당한다.
  2. 셀의 width, height을 정한다.
  3. 필요한 경우 셀들 간의 좌우 최소 간격, 위아래 최소 간격을 설정한다.
  4. 섹션에 header와 footer가 있다면 이것들의 크기를 지정한다.
  5. 레이아웃의 스크롤 방향을 설정한다.

UICollectionViewDataSource

컬렉션 뷰로 보여지는 컨텐츠들을 관리하는 객체

UICollectionViewDelegate

컨텐츠의 표현, 사용자와의 상호 작용과 관련된 것들을 관리하는 객체

구현

  • 일기 작성하는 화면에서 내용 넣는 Text View 부분의 테두리가 없어서 configureContentsTextView 함수 구현
    - layer와 관련된 색상을 설정할 때는 cgColor 를 사용해야 한다.
    - viewDidLoad 함수에 해당 함수를 선언하여 적용
private func configureContentsTextView() {
        let borderColor = UIColor(red: 220/255, green: 220/255, blue: 220/255, alpha: 1.0)
        self.contentsTextView.layer.borderColor = borderColor.cgColor
        self.contentsTextView.layer.borderWidth = 0.5
        self.contentsTextView.layer.cornerRadius = 5.0
    }
  • 일기 작성하는 화면에서 날짜 선택하는 부분을 누르면 날짜를 스크롤 화면으로 고를 수 있도록 하기 위해 configureDatePicker 함수 구현
    - UIDatePicker 사용
    - viewDidLoad 함수에 해당 함수를 선언하여 적용
private let datePicker = UIDatePicker()
private var diaryDate: Date?

private func configureDatePicker() {
    self.datePicker.datePickerMode = .date
    self.datePicker.preferredDatePickerStyle = .wheels
    self.datePicker.addTarget(self, action: #selector(datePickerValueDidChange(_:)), for: .valueChanged)
    self.datePicker.locale = Locale(identifier: "ko_KR")
    self.dateTextField.inputView = self.datePicker
}

@objc private func datePickerValueDidChange(_ datePicker: UIDatePicker) {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy년 MM월 dd일(EEEEE)"
    formatter.locale = Locale(identifier: "ko_KR")
    self.diaryDate = datePicker.date
    self.dateTextField.text = formatter.string(from: datePicker.date)
    self.dateTextField.sendActions(for: .editingChanged)
    // 아래에서 내용을 다 입력해야지만 등록 버튼을 활성화할 수 있도록 하는 구현을 할 때 날짜를 선택하는 경우, UIDatePicker를 사용했기 때문에 dateTextFieldDidChange 함수가 호출되지 않아 해당 부분을 써주었다.
}
  • 화면의 빈 부분을 누르면 다른 화면(Ex. 키보드 화면)이 꺼지도록 구현
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    self.view.endEditing(true)
}
  • 입력해야 할 내용을 다 입력해야지만 등록 버튼이 활성화되도록 구현
    - viewDidLoad 함수에서 초기 화면에서는 등록 버튼이 비활성화된 상태로 시작할 수 있도록 구현
    - viewDidLoad 함수에 configureInputField 함수를 선언하여 구현
private func configureInputField() {
    self.contentsTextView.delegate = self
    self.titleTextField.addTarget(self, action: #selector(titleTextFieldDidChange(_:)), for: .editingChanged)
    self.dateTextField.addTarget(self, action: #selector(dateTextFieldDidChange(_:)), for: .editingChanged)
}

@objc private func titleTextFieldDidChange(_ textField: UITextField) {
    self.validateInputField()
}
    
@objc private func dateTextFieldDidChange(_ textField: UITextField) {
    self.validateInputField()
}

private func validateInputField() {
    self.confirmButton.isEnabled = !(self.titleTextField.text?.isEmpty ?? true) && !(self.dateTextField.text?.isEmpty ?? true) && !self.contentsTextView.text.isEmpty
    // text가 없는 경우 nil이기 때문에 옵셔널 선언을 해주고, Bool 판단을 위해 nil일 경우 true를 저장해주었다.
}

//WriteDiaryViewController 클래스 밖에 위치
extension WriteDiaryViewController: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        self.validateInputField()
    }
}
  • Delegate 패턴을 사용하여 일기를 작성하고 등록하면 ViewController 에서 보일 수 있도록 구현

ViewController

protocol WriteDiaryViewDelegate: AnyObject {
    func didSelectRegister(diary: Diary)
}

// ViewController 클래스 내부에 구현
// diaryList에 diary가 저장될 때마다 saveDiaryList 함수가 호출되도록 하여 데이터 유지
private var diaryList = [Diary]() {
	didSet {
    	self.saveDiaryList()
    }
}

// ViewController 클래스 내부에 구현
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if let writeDiaryViewController = segue.destination as? WriteDiaryViewController {
        writeDiaryViewController.delegate = self
    }
}

extension ViewController: WriteDiaryViewDelegate {
    func didSelectRegister(diary: Diary) {
        self.diaryList.append(diary)
        // diary가 날짜 최신순으로 정렬되도록 구현
    	self.diaryList = self.diaryList.sorted(by: {
        	$0.date.compare($1.date) == .orderedDescending
    	})
        self.collectionView.reloadData()
        // 일기가 추가되면 collectionView에서 확인할 수 있도록 구현
    }
}

WriteDiaryViewController

weak var delegate: WriteDiaryViewDelegate?

@IBAction func tapConfirmButton(_ sender: UIBarButtonItem) {
    guard let title = self.titleTextField.text else { return }
    guard let contents = self.contentsTextView.text else { return }
    guard let date = self.diaryDate else { return }
    let diary = Diary(title: title, contents: contents, date: date, isStar: false)
    self.delegate?.didSelectRegister(diary: diary)
    self.navigationController?.popViewController(animated: true)
}
  • CollectionView 를 사용하여 일기 목록을 확인할 수 있도록 구현
    - viewDidLoad 함수에 configureCollectionView 함수를 선언하여 구현

ViewController

private func configureCollectionView() {
    self.collectionView.collectionViewLayout = UICollectionViewFlowLayout()
    self.collectionView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    self.collectionView.delegate = self
    self.collectionView.dataSource = self
}

private func dateToString(date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "yy년 MM월 dd일(EEEEE)"
    formatter.locale = Locale(identifier: "ko_KR")
    return formatter.string(from: date)
}

// ViewController 클래스 밖에 구현
extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.diaryList.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DiaryCell", for: indexPath) as? DiaryCell else { return UICollectionViewCell() }
        let diary = self.diaryList[indexPath.row]
        cell.titleLabel.text = diary.title
        cell.dateLabel.text = self.dateToString(date: diary.date)
        return cell
    }
}

// ViewController 클래스 밖에 구현
extension ViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: (UIScreen.main.bounds.width / 2) - 20, height: 200)
    }
}
  • UserDefaults 를 사용하여 앱을 종료했다가 재실행하더라도 데이터가 유지되도록 구현
    - viewDidLoad 함수에 loadDiaryList 함수를 선언하여 앱을 실행할 때 저장되어 있던 일기가 보여질 수 있도록 구현

ViewController

private func saveDiaryList() {
    let diary = self.diaryList.map {
        [
            "title": $0.title,
            "contents": $0.contents,
            "date": $0.date,
            "isStar": $0.isStar
        ]
    }
    let userDefaults = UserDefaults.standard
    userDefaults.set(diary, forKey: "diaryList")
}
    
private func loadDiaryList() {
    let userDefaults = UserDefaults.standard
    guard let data = userDefaults.object(forKey: "diaryList") as? [[String : Any]] else { return }
    self.diaryList = data.compactMap {
        guard let title = $0["title"] as? String else { return nil }
        guard let contents = $0["contents"] as? String else { return nil }
        guard let date = $0["date"] as? Date else { return nil }
        guard let isStar = $0["isStar"] as? Bool else { return nil }
        return Diary(title: title, contents: contents, date: date, isStar: isStar)
    }
    
    // diary가 날짜 최신순으로 정렬되도록 구현
    self.diaryList = self.diaryList.sorted(by: {
        $0.date.compare($1.date) == .orderedDescending
    })
}
  • 일기를 구분하기 위해 테두리 적용
    - required init 은 UI가 생성될 때 호출되는 생성자라고 한다.

DiaryCell

required init?(coder: NSCoder) {
    super.init(coder: coder)
    self.contentView.layer.cornerRadius = 3.0
    self.contentView.layer.borderWidth = 1.0
    self.contentView.layer.borderColor = UIColor.black.cgColor
}
  • 일기가 선택되었을 때 일기의 상세 정보를 확인할 수 있도록 구현
  • Delegate 를 사용하여 삭제된 일기의 indexPath 정보를 ViewController 에 전달하여 삭제 기능 구현

DiaryDetailViewController

protocol DiaryDetailViewDelegate: AnyObject {
    func didSelectDelete(indexPath: IndexPath)
}

class DiaryDetailViewController: UIViewController {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var contentsTextView: UITextView!
    @IBOutlet weak var dateLabel: UILabel!
    weak var delegate: DiaryDetailViewDelegate?
    
    var diary: Diary?
    var indexPath: IndexPath?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.configureView()
    }
    
    private func configureView() {
        guard let diary = self.diary else { return }
        self.titleLabel.text = diary.title
        self.contentsTextView.text = diary.contents
        self.dateLabel.text = self.dateToString(date: diary.date)
    }
    
    private func dateToString(date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yy년 MM월 dd일(EEEEE)"
        formatter.locale = Locale(identifier: "ko_KR")
        return formatter.string(from: date)
    }
    
    @IBAction func tapEditButton(_ sender: UIButton) {
    }
    
    @IBAction func tapDeleteButton(_ sender: UIButton) {
        guard let indexPath = self.indexPath else { return }
        self.delegate?.didSelectDelete(indexPath: indexPath)
        self.navigationController?.popViewController(animated: true)
    }
}

ViewController

extension ViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let viewController = self.storyboard?.instantiateViewController(withIdentifier: "DiaryDetailViewController") as? DiaryDetailViewController else { return }
        let diary = self.diaryList[indexPath.row]
        viewController.diary = diary
        viewController.indexPath = indexPath
        viewController.delegate = self
        self.navigationController?.pushViewController(viewController, animated: true)
    }
}

extension ViewController: DiaryDetailViewDelegate {
    func didSelectDelete(indexPath: IndexPath) {
        self.diaryList.remove(at: indexPath.row)
        self.collectionView.deleteItems(at: [indexPath])
    }
}

Delegate 어렵다

  1. 일기를 작성하면 메인 화면에서 확인
    • 실제 구현 ViewController
  2. 일기 상세 화면에서 삭제 누르면 메인 화면에서 일기 삭제
    • 실제 구현 ViewController

둘의 차이점이 protocol 의 위치였는데 protocol 의 위치는 상관없는 것 같다.(이거 때문에 헷갈렸다.)
둘 다 다른 화면에서 정보를 받아 와서 결국 구현은 ViewController 에서 한다.

다시 구현

  • 일기 상세 화면에서 수정 버튼 누르면 일기 작성 화면으로 넘어가고 기존의 내용들도 보일 수 있도록 구현
    - WriteDiaryViewController 에 열거형 추가
    - viewDidLoad 함수에 configureEditMode 함수를 선언하여 구현

  • NotificationCenter 를 이용하여 일기의 수정이 일어나면 일기 상세 화면에도 반영하고 메인 화면에도 반영되도록 구현(NotificationCenter 를 사용하면 특정 이벤트가 발생하는 것을 observing 할 수 있다.)
    - ViewControllerviewDidLoad 함수에서 NotificationCenter 를 사용하여 수정이 일어나면 메인 화면에도 변경이 일어나도록 구현
    - deinit 으로 NotificationCenter observer 삭제

WriteDiaryViewController

//클래스 밖에 구현
enum DiaryEditorMode {
    case new
    case edit(IndexPath, Diary)
}

var diaryEditorMode: DiaryEditorMode = .new

private func configureEditMode() {
    switch self.diaryEditorMode {
    case let .edit(_, diary):
        self.titleTextField.text = diary.title
        self.contentsTextView.text = diary.contents
        self.dateTextField.text = self.dateToString(date: diary.date)
        self.diaryDate = diary.date
        self.confirmButton.title = "수정"
    default:
        break
    }
}
    
private func dateToString(date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "yy년 MM월 dd일(EEEEE)"
    formatter.locale = Locale(identifier: "ko_KR")
    return formatter.string(from: date)
}

@IBAction func tapConfirmButton(_ sender: UIBarButtonItem) {
    guard let title = self.titleTextField.text else { return }
    guard let contents = self.contentsTextView.text else { return }
    guard let date = self.diaryDate else { return }
    let diary = Diary(title: title, contents: contents, date: date, isStar: false)
        
    switch self.diaryEditorMode {
    case .new:
        self.delegate?.didSelectRegister(diary: diary)
    case let .edit(indexPath, _):
        NotificationCenter.default.post(name: NSNotification.Name("editDiary"), object: diary, userInfo: [
            "indexPath.row": indexPath.row
        ])
    }
        
    self.navigationController?.popViewController(animated: true)
}

DiaryDetailViewController

@IBAction func tapEditButton(_ sender: UIButton) {
    guard let viewController = self.storyboard?.instantiateViewController(withIdentifier: "WriteDiaryViewController") as? WriteDiaryViewController else { return }
    guard let indexPath = self.indexPath else { return }
    guard let diary = self.diary else { return }
    viewController.diaryEditorMode = .edit(indexPath, diary)
    NotificationCenter.default.addObserver(self, selector: #selector(editDiaryNotification(_:)), name: NSNotification.Name("editDiary"), object: nil)
    self.navigationController?.pushViewController(viewController, animated: true)
}

@objc func editDiaryNotification(_ notification: Notification) {
    guard let diary  = notification.object as? Diary else { return }
    guard let row = notification.userInfo?["indexPath.row"] as? Int else { return }
    self.diary = diary
    self.configureView()
}

deinit {
    NotificationCenter.default.removeObserver(self)
}

ViewController

override func viewDidLoad() {
    super.viewDidLoad()
    self.configureCollectionView()
    self.loadDiaryList()
    NotificationCenter.default.addObserver(self, selector: #selector(editDiaryNotification(_:)), name: NSNotification.Name("editDiary"), object: nil)
}

@objc func editDiaryNotification(_ notification: Notification) {
    guard let diary = notification.object as? Diary else { return }
    guard let row = notification.userInfo?["indexPath.row"] as? Int else { return }
    self.diaryList[row] = diary
    self.diaryList = self.diaryList.sorted(by: {
        $0.date.compare($1.date) == .orderedDescending
    })
    self.collectionView.reloadData()
}

즐겨찾기 기능 구현

  • 일기 상세 화면에 즐겨찾기 버튼 생성 및 즐겨찾기 상태가 유지되도록 delegate를 이용하여 구현

DiaryDetailViewController

// 클래스 밖에 구현
protocol DiaryDetailViewDelegate: AnyObject {
    func didSelectDelete(indexPath: IndexPath)
    func didSelectStar(indexPath: IndexPath, isStar: Bool)
}

var starButton: UIBarButtonItem?

private func configureView() {
    guard let diary = self.diary else { return }
    self.titleLabel.text = diary.title
    self.contentsTextView.text = diary.contents
    self.dateLabel.text = self.dateToString(date: diary.date)
    self.starButton = UIBarButtonItem(image: nil, style: .plain, target: self, action: #selector(tapStarButton))
    self.starButton?.image = diary.isStar ? UIImage(systemName: "star.fill") : UIImage(systemName: "star")
    self.starButton?.tintColor = .orange
    self.navigationItem.rightBarButtonItem = self.starButton
}

@objc func tapStarButton() {
    guard let isStar = self.diary?.isStar else { return }
    guard let indexPath = self.indexPath else { return }
    if isStar {
        self.starButton?.image = UIImage(systemName: "star")
    } else {
        self.starButton?.image = UIImage(systemName: "star.fill")
    }
    self.diary?.isStar = !isStar
    self.delegate?.didSelectStar(indexPath: indexPath, isStar: self.diary?.isStar ?? false)
}

ViewController

extension ViewController: DiaryDetailViewDelegate {
    func didSelectDelete(indexPath: IndexPath) {
        self.diaryList.remove(at: indexPath.row)
        self.collectionView.deleteItems(at: [indexPath])
    }
    
    func didSelectStar(indexPath: IndexPath, isStar: Bool) {
        self.diaryList[indexPath.row].isStar = isStar
    }
}
  • 즐겨찾기된 일기 리스트를 확인할 수 있도록 구현
    - UserDefaults 로 저장된 일기 리스트를 불러오고 이를 여러 고차 함수들을 이용하여 즐겨찾기된 일기 리스트만 불러올 수 있도록 하였다.
    • Cell 의 테두리 구분

StarViewController

class StarViewController: UIViewController {
    @IBOutlet weak var collectionView: UICollectionView!
    
    private var diaryList = [Diary]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.configureCollectionView()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.loadStarDiaryList()
    }
    
    private func dateToString(date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yy년 MM월 dd일(EEEEE)"
        formatter.locale = Locale(identifier: "ko_KR")
        return formatter.string(from: date)
    }
    
    private func configureCollectionView() {
        self.collectionView.collectionViewLayout = UICollectionViewFlowLayout()
        self.collectionView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        self.collectionView.delegate = self
        self.collectionView.dataSource = self
    }
    
    private func loadStarDiaryList() {
        let userDefaults = UserDefaults.standard
        guard let data = userDefaults.object(forKey: "diaryList") as? [[String: Any]] else { return }
        self.diaryList = data.compactMap {
            guard let title = $0["title"] as? String else { return nil }
            guard let contents = $0["contents"] as? String else { return nil }
            guard let date = $0["date"] as? Date else { return nil }
            guard let isStar = $0["isStar"] as? Bool else { return nil }
            return Diary(title: title, contents: contents, date: date, isStar: isStar)
        }.filter({
            $0.isStar == true
        }).sorted(by: {
            $0.date.compare($1.date) == .orderedDescending
        })
        self.collectionView.reloadData()
    }
}

extension StarViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.diaryList.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "StarCell", for: indexPath) as? StarCell else { return UICollectionViewCell() }
        let diary = self.diaryList[indexPath.row]
        cell.titleLabel.text = diary.title
        cell.dateLabel.text = self.dateToString(date: diary.date)
        return cell
    }
}

extension StarViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: UIScreen.main.bounds.width - 20, height: 80)
    }
}

StarCell

class StarCell: UICollectionViewCell {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var dateLabel: UILabel!
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.contentView.layer.cornerRadius = 3.0
        self.contentView.layer.borderWidth = 1.0
        self.contentView.layer.borderColor = UIColor.black.cgColor
    }
}
  • 즐겨찾기 화면에서 일기를 선택하면 해당 일기에 대한 상세 정보를 확인할 수 있도록 구현

StarViewController

extension StarViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let viewController = self.storyboard?.instantiateViewController(withIdentifier: "DiaryDetailViewController") as? DiaryDetailViewController else { return }
        let diary = self.diaryList[indexPath.row]
        viewController.diary = diary
        viewController.indexPath = indexPath
        self.navigationController?.pushViewController(viewController, animated: true)
    }
}
  • 즐겨찾기 여부에 따라 메인 화면과 즐겨찾기 화면에서 일기 목록이 생기거나 사라지거나 해야 하는데 현재는 delegate 로 구현되어 있기 때문에 1:1 로 밖에 대응이 되지 않는다. 따라서 NotificationCenter 를 사용하여 이를 구현하였다.
    - 현재 상태에서는 IndexPath 를 사용하여 일기의 수정, 삭제 등을 하고 메인 화면, 즐겨찾기 화면 모두 NotificationCenter 를 사용하기 때문에 만약 메인 화면에는 있지만 즐겨찾기 화면에는 없는 일기를 삭제하는 경우, index out of range 에러가 발생하게 된다. 이를 아래에서 처리해 줄 예정이다.

DiaryDetailViewController

@objc func tapStarButton() {
    guard let isStar = self.diary?.isStar else { return }
    guard let indexPath = self.indexPath else { return }
    if isStar {
        self.starButton?.image = UIImage(systemName: "star")
    } else {
        self.starButton?.image = UIImage(systemName: "star.fill")
    }
    self.diary?.isStar = !isStar
    NotificationCenter.default.post(name: NSNotification.Name("starDiary"), object: [
    	"diary": self.diary,
        "isStar": self.diary?.isStar ?? false,
        "indexPath": indexPath
    ], userInfo: nil)
}

@IBAction func tapDeleteButton(_ sender: UIButton) {
    guard let indexPath = self.indexPath else { return }
    NotificationCenter.default.post(name: NSNotification.Name("deleteDiary"), object: indexPath, userInfo: nil)
    self.navigationController?.popViewController(animated: true)
}

ViewController

override func viewDidLoad() {
    super.viewDidLoad()
    self.configureCollectionView()
    self.loadDiaryList()
    NotificationCenter.default.addObserver(self, selector: #selector(editDiaryNotification(_:)), name: NSNotification.Name("editDiary"), object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(starDiaryNotification(_:)), name: NSNotification.Name("starDiary"), object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(deleteDiaryNotification(_:)), name: NSNotification.Name("deleteDiary"), object: nil)
}

@objc func starDiaryNotification(_ notification: Notification) {
    guard let starDiary = notification.object as? [String: Any] else { return }
    guard let isStar = starDiary["isStar"] as? Bool else { return }
    guard let indexPath = starDiary["indexPath"] as? IndexPath else { return }
    self.diaryList[indexPath.row].isStar = isStar
}
    
@objc func deleteDiaryNotification(_ notification: Notification) {
    guard let indexPath = notification.object as? IndexPath else { return }
    self.diaryList.remove(at: indexPath.row)
    self.collectionView.deleteItems(at: [indexPath])
}

StarViewController

override func viewDidLoad() {
    super.viewDidLoad()
    self.configureCollectionView()
    self.loadStarDiaryList()
    NotificationCenter.default.addObserver(self, selector: #selector(editDiaryNotification(_:)), name: NSNotification.Name("editDiary"), object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(starDiaryNotification(_:)), name: NSNotification.Name("starDiary"), object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(deleteDiaryNotification(_:)), name: NSNotification.Name("deleteDiary"), object: nil)
}

@objc func editDiaryNotification(_ notification: Notification) {
    guard let diary = notification.object as? Diary else { return }
    guard let row = notification.userInfo?["indexPath.row"] as? Int else { return }
    self.diaryList[row] = diary
    self.diaryList = self.diaryList.sorted(by: {
        $0.date.compare($1.date) == .orderedDescending
    })
    self.collectionView.reloadData()
}
    
@objc func starDiaryNotification(_ notification: Notification) {
    guard let starDiary = notification.object as? [String: Any] else { return }
    guard let diary = starDiary["diary"] as? Diary else { return }
    guard let isStar = starDiary["isStar"] as? Bool else { return }
    guard let indexPath = starDiary["indexPath"] as? IndexPath else { return }
    if !isStar {
        self.diaryList.remove(at: indexPath.row)
        self.collectionView.deleteItems(at: [indexPath])
    } else {
        self.diaryList.append(diary)
        self.diaryList = self.diaryList.sorted(by: {
            $0.date.compare($1.date) == .orderedDescending
        })
        self.collectionView.reloadData()
    }
}
    
@objc func deleteDiaryNotification(_ notification: Notification) {
    guard let indexPath = notification.object as? IndexPath else { return }
    self.diaryList.remove(at: indexPath.row)
    self.collectionView.deleteItems(at: [indexPath])
}
  • 위에서 발생한 문제를 UUID 라는 고유한 값을 일기에 부여하여 해결하고자 하였다.
    - NotificationCenter 에서 indexPath 로 처리한 로직을 모두 UUID 로 바꿔주었다.

Diary

struct Diary {
    var uuidString: String
    var title: String
    var contents: String
    var date: Date
    var isStar: Bool
}

최종 화면



GitHub

https://github.com/pjs0418/Diary

출처

패스트캠퍼스, 초격차 패키지 : 30개 프로젝트로 배우는 iOS 앱 개발 with Swift

좋은 웹페이지 즐겨찾기