61일차 TIL _ 팀프로젝트 회고
거북이의 단어장 TURTLE VOCA
프로젝트를 하면서 어려웠던 점
- 팀원분의 추천으로 View를 top, body, bottom으로 나눠서 진행했는데, ViewController에서만 할 수 있는 기능들을 추가하기 위해 연결하는 부분에서 어려움을 겪었다. (처음엔 저번 개인과제 때 사용해 봐서 익숙한 delegate 방식을 많이 사용했다. 그러나 여러 파일에 데이터를 전달해야 할 때 너무 헷갈리고, 복잡하다는 생각이 들었다. 그리고 이 방식 외에도 다른 간편한 방식이 있다는 것을 알게 되었음)
1-1. Delegate 방식
//AddBookCaseBodyView.swift
protocol AddBookCaseBodyViewDelegate: AnyObject {
func didSelectImage()
}
이미지 선택 시 PHPicker를 띄우기 -> PHPicker를 present하려면 ViewController에 해줘야 하기 때문에 protocol을 사용해서 연결해 준다.
//AddBookCaseViewController.swift
extension AddBookCaseViewController: AddBookCaseBodyViewDelegate {
//이미지 선택시 PHPicker present
func didSelectImage() {
var configuration = PHPickerConfiguration()
configuration.filter = .images
configuration.selectionLimit = 1
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
present(picker, animated: true, completion: nil)
}
}
이렇게 extention을 사용해 줘서 Delegate 할 함수들을 따로 분류할 수 있음. 이렇게 Delegate 패턴으로 연결해서 사용하는 방법이 있다.
1-2. NotificationCenter 방식
여러 객체가 동일한 이벤트에 응답해야 할 때 매우 유용
func setupKeyboardEvent() {
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillHide),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}
@objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
let keyboardHeight = keyboardSize.height
// 키보드가 나타날 때 화면을 이동시키는 코드
// 예를 들어, 키보드에 따라서 frame을 조정하는 방식 (키보드가 올라올 때 뷰를 위로 이동)
}
}
@objc func keyboardWillHide(notification: NSNotification) {
// 키보드가 사라질 때 화면을 원래 상태로 되돌리는 코드
// 예를 들어, 키보드에 따라서 frame을 원래 위치로 되돌리는 방식
}
이 함수의 경우 키보드가 올라올 때 텍스트 필드를 가리지 않도록 frame의 위치를 위로 이동시키는 기능을 구현할 때 사용했는데,
이렇게 NotificationCenter를 사용해 주면 한 번만 코드를 적어도 모든 뷰 컨트롤러에 동일한 이벤트 핸들러를 설정할 수 있고, 이벤트를 수신하고 적절하게 대응할 수 있음.
//AddBookCaseViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupConstraints()
bodyView.delegate = self
setupKeyboardEvent()
}
이렇게 이 기능이 필요한 ViewController에 viewDidLoad에 호출해 주어서 간단하게 사용할 수 있다!
import Foundation
extension Notification.Name {
static let sender = Notification.Name("sender")
static let getData = Notification.Name("getData")
static let count = Notification.Name("count")
}
* 아 그리고 팀원분의 꿀팁인데 Notification이름을 이런 식으로 static으로 저장해서 사용하면 좋다고 하셨다. 그럼 사용할 때는 네임 부분에 . 만 찍어도 이름이 자동으로 나와준다.
그리고 NotificationCenter와 Delegate 패턴의 차이점에 대해서 간단하게 이야기해보자면,
- NotificationCenter
: 이벤트를 여러 객체에게 브로드캐스트 하는 데 적합하며, 발신자와 수신자 간의 결합이 느슨하다.
(but 디버깅이 어렵고, 성능 저하 가능성이 있음)
- Delegate 패턴
: 1:1 통신에 적합하며, 명확한 역할 분담과 직접적인 통신이 가능. ( but 발신자와 수신자 간의 결합도가 높아지고, 확장성 제한)
1-3. UIResponder 사용
https://developer.apple.com/documentation/uikit/uiresponder
iOS 이벤트 처리의 기본 클래스. 터치 이벤트, 모션 이벤트, 리모트 컨트롤 이벤트 등을 처리하는 객체가 이를 상속
import UIKit
extension UIResponder {
var currentViewController: UIViewController? {
return next as? UIViewController ?? next?.currentViewController
}
}
//BookCaseHeaderView.swift
@objc func plusButtonTapped() {
let addBookCaseVC = AddBookCaseViewController()
addBookCaseVC.modalPresentationStyle = .fullScreen
currentViewController?.present(addBookCaseVC, animated: true, completion: nil)
}
//AddBookCaseHeaderView.swift
@objc func backButtonTapped() {
currentViewController?.dismiss(animated: true)
}
이런 식의 접근도 가능하고, 포커스 받고 있는 텍스트 필드를 찾을 때도 사용해 주었다.
//MARK: - 포커스 받고 있는 텍스트 필드 찾기
extension UIResponder {
private static weak var currentResponder: UIResponder?
public static func findFirstResponder() -> UIResponder? {
UIResponder.currentResponder = nil
UIApplication.shared.sendAction(#selector(UIResponder._trap), to: nil, from: nil, for: nil)
return UIResponder.currentResponder
}
@objc private func _trap() {
UIResponder.currentResponder = self
}
}
세 패턴 비교
다들 장단점이 있기 때문에 상황에 맞춰서 알맞게 사용하는 것이 중요하다고 생각했다.
정리해 보자면 여러 뷰 컨트롤러에서 공통으로 처리해야 하는 경우 NotificationCenter를 사용하고, 특정 객체 간의 1:1 통신이 필요할 때는 Delegate를 사용, 그리고 현재 응답 중인 객체를 찾고자 할 때는 UIResponder를 사용하는 것이 적절하다.
* present를 ViewController에서만 해줄 수 있는 이유는 present(_:animated:completion:) 메서드가 UIViewController 클래스의 인스턴스 메서드이기 때문이다. UIViewController를 상속받은 클래스의 인스턴스에서만 호출할 수 있음
뷰 계층 구조 관리
: 뷰 컨트롤러는 화면에 표시되는 뷰의 계층 구조를 관리한다.
새로운 뷰 컨트롤러를 표시하는 작업(ex. present 메서드)은 기존의 뷰 계층 구조에 새로운 뷰를 추가하는 것을 의미.
이 작업은 뷰 컨트롤러에서 처리할 수 있다.
뷰 생명주기 관리
: 뷰 컨트롤러는 뷰의 생명주기(lifecycle)를 관리합니다.
새로운 뷰 컨트롤러를 모달로 표시하면 표시되는 뷰 컨트롤러와 기존 뷰 컨트롤러 모두의 생명주기 메서드가 호출.
이러한 생명주기 관리 또한 뷰 컨트롤러에서 처리할 수 있다.
전환 애니메이션
: present 메서드는 뷰 컨트롤러 간의 전환 애니메이션을 처리한다.
이러한 애니메이션은 뷰 계층 구조 및 화면의 현재 상태와 밀접하게 관련되어 있기 때문에 뷰 컨트롤러에서만 관리할 수 있습니다.
2. 오토레이아웃 ( 항상 어렵고 내가 생각한 대로 나오지 않는 것 같다.. 기존 어플을 따라 하는 연습을 많이 해봐야 할 거 같음)
3. UIMenu 디자인 ( 구현하는 것은 생각보다 괜찮았지만, UIMenu를 디자인하고 싶었는데, 찾아보니까 UIMenu는 디자인할 수가 없다고 나와있었다. 그런데 다른 앱들을 보면 꾸며져 있는 것들이 있어서 UIMenu가 아니면 어떤 방식으로 구현되는지.. 그리고 커스텀하는 방식이 궁금했다.. )
5. 코어데이터 relationship ( + delete rule ) -> 이건 전에 올린 TIL에 잘 정리했음..
6. CollectionView에 애니메이션 넣기 ( Carousel Effect )
7. 생명주기 (이론으로 배웠을 때는 솔직히 잘 이해가 안 갔는데 조금 이해가 된 거 같다 )
ㄴ 전에 개인과제를 했을 때 튜터님이 피드백으로 뷰의 라이프 사이클을 잘 활용해서 viewWillAppear 시점에 reload를 시키는 것도 잘해주셨습니다! 한 가지 더 보태자면 지금의 방식이 당연히 틀린 건 아니지만 데이터의 변경이 없어도 화면에 진입할 때마다 불필요한 reload도 발생할 수 있겠다는 생각이 들었습니다 :) 어떻게 하면 이를 해결할지도 고민해 보세요~!
라고 말해주셨는데, 이번에 컬렉션뷰를 reload 하면서 NotificationCenter를 통해서 reload 해주면 불필요한 reload를 없앨 수 있다!
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(didBookCase), name: NSNotification.Name("didBookCase"), object: nil)
}
// 단어장 추가 시 relaod
@objc func didBookCase() {
bodyView.configureUI()
}
extension AddBookCaseViewController: AddBookCaseBodyViewDelegate {
// 저장 완료 시
func addButtonTapped() {
let alertController = AlertController().makeAlertWithCompletion(title: "저장 완료", message: "단어장이 저장되었습니다.") { _ in
NotificationCenter.default.post(name: NSNotification.Name("didBookCase"), object: nil)
self.dismiss(animated: true, completion: nil)
}
present(alertController, animated: true, completion: nil)
}
extension EditBookCaseViewController: EditBookCaseBodyViewDelegate {
// 수정 완료 시
func editButtonTapped() {
let alertController = AlertController().makeAlertWithCompletion(title: "수정 완료", message: "단어장이 수정되었습니다.") { _ in
NotificationCenter.default.post(name: NSNotification.Name("didBookCase"), object: nil)
self.dismiss(animated: true, completion: nil)
}
present(alertController, animated: true, completion: nil)
}
이렇게 해줘서 저장, 수정 완료 시에 reload를 해줘서 불필요한 reload를 줄일 수 있음!
인상 깊었던 점
- 원래 ImagePicker를 사용했었는데 PHPicker을 앞으로 쓰게 될 것이라는 정보를 얻었다!
- UICollectionViewDiffableDataSource
- Label Factory를 만들어서 다 같이 간편하게 사용했던 점
- extension 사용해서 코드 정리(분리?)
- Toolbar
- @escaping으로 에러 처리
- 스택뷰 사용
- README 작성
아쉬웠던 점
- 로그인 기능 도전 해보고 싶었는데 아쉽다
- API 사용도 아직 미숙해서 더 공부해보고 싶었는데 아쉽다
- 더 추가하고 싶었던 기능이 있는데 아쉽다 ( 단어장 추가 시 중복내용 확인, 단어장 전체 삭제 기능, 단어장 고정 기능, 텍스트 필드 작성 시 count 되는 기능, Notification)
- MVVM 해보고 싶었는데 아쉽다
그래도 이렇게 끝나지 않고, 팀원들과 계속 개선해 나가기로 해서 좋았다!
앞으로 최종프로젝트가 남았는데 좋은 사람들과 함께 이번 프로젝트에서처럼 많이 배울 수 있었으면 좋겠다고 생각했다..!
화이티이이잉 !! 😆
https://github.com/SijongKim93/Vocabulary
p.s 개발자의 길은 멀고도 험한 것 같다... ⭐️