(iOS)일기 앱 만들기
기능 상세
- 일기장 탭을 누르면 일기 리스트 표시
- 즐겨찾기 탭을 누르면 즐겨찾기한 일기 리스트 표시
- 일기 등록, 수정, 삭제, 즐겨찾기 기능 구현
활용 기술
- UITabBarController
- UICollectionView
- NotificationCenter
UITabBarController
UITabBar
- UITabBarController
- UICollectionView
- NotificationCenter
UITabBarController
UITabBar
앱에서 서로 다른 하위 작업, 뷰, 모드 사이의 선택을 할 수 있도록 탭바에 하나 혹은 하나 이상의 버튼을 보여주는 컨트롤
UITabBarController
다중 선택 인터페이스를 관리하는 뷰 컨트롤러
선택에 따라 어떤 자식 뷰 컨트롤러를 보여줄 것인지 결정
UICollectionView
데이터 항목의 정렬된 컬렉션을 관리하고 커스텀한 레이아웃을 사용해 표시하는 객체
구성 요소
Cell
컬렉션 뷰의 컨텐츠 표시
Supplementary View(필수 X)
섹션에 대한 정보 표시
Decoration View
컬렉션 뷰에 대한 배경을 꾸밀 때 사용
UICollectionViewFlowLayout
- Flow 레이아웃 객체를 작성하고 컬렉션 뷰에 이를 할당한다.
- 셀의 width, height을 정한다.
- 필요한 경우 셀들 간의 좌우 최소 간격, 위아래 최소 간격을 설정한다.
- 섹션에 header와 footer가 있다면 이것들의 크기를 지정한다.
- 레이아웃의 스크롤 방향을 설정한다.
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
에서 보일 수 있도록 구현
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 함수가 호출되지 않아 해당 부분을 써주었다.
}
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 어렵다
- 일기를 작성하면 메인 화면에서 확인
- 실제 구현
ViewController
- 실제 구현
- 일기 상세 화면에서 삭제 누르면 메인 화면에서 일기 삭제
- 실제 구현
ViewController
- 실제 구현
둘의 차이점이 protocol
의 위치였는데 protocol
의 위치는 상관없는 것 같다.(이거 때문에 헷갈렸다.)
둘 다 다른 화면에서 정보를 받아 와서 결국 구현은 ViewController
에서 한다.
다시 구현
-
일기 상세 화면에서 수정 버튼 누르면 일기 작성 화면으로 넘어가고 기존의 내용들도 보일 수 있도록 구현
-WriteDiaryViewController
에 열거형 추가
-viewDidLoad
함수에configureEditMode
함수를 선언하여 구현 -
NotificationCenter
를 이용하여 일기의 수정이 일어나면 일기 상세 화면에도 반영하고 메인 화면에도 반영되도록 구현(NotificationCenter
를 사용하면 특정 이벤트가 발생하는 것을 observing 할 수 있다.)
-ViewController
의viewDidLoad
함수에서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
Author And Source
이 문제에 관하여((iOS)일기 앱 만들기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@tony1803/iOS-일기-앱-만들기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)