# 우리뭐먹지 PR ## Wiki - `PR로 협업하기`를 통해 프로젝트를 관리했습니다. - 새로운 Feature를 추가할 때마다 아래의 양식으로 PR을 작성하여 프로젝트 히스토리 (배경, 작업 내용, 테스트 방법 등)를 관리합니다. - PR Label (bug, emergency, feature)을 사용하여 작업을 구분했습니다. - PR 리스트 # [PR-1. Feature/landing: 앱을 처음 실행하는 경우 온보딩 페이지를 실행하도록 합니다](https://github.com/just1103/WhatWeEat/pull/1) # 배경 사용자가 앱을 처음 사용하는 경우, 앱의 기능과 사용 방법을 소개하기 위해 `OnboardingPage`를 구현했습니다. 1~2페이지는 앱에 대한 설명을 나타내고, 3페이지에는 사용자가 못먹는음식을 제출하도록 했습니다. # 작업 내용 ## 1. PageViewController를 통한 OnboardingPage 관리 `PageViewController`는 Child로 3개 페이지를 가집니다. 또한 PageControl을 통해 사용자가 쉽게 현재 페이지 위치를 알 수 있습니다. ## 2. 못먹는음식 화면에서 버튼 isHidden 처리 Onboarding 화면 중 못먹는음식 화면에선 `PageControl`과 `skip` 버튼이 사라지고 `확인` 버튼만 보이도록 했습니다. 기존에는 `pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted)` 메서드를 활용해 버튼이 사라지도록 했지만, 이 경우 버튼이 Scroll 이후 뒤늦게 사라져 UX에 좋지 않다고 판단했습니다. 따라서 `pageViewController(_:willTransitionTo pendingViewControllers:)` 메서드에서 `pendingViewControllers`를 통해 화면이 전환되려할 때 버튼이 사라지도록 했습니다. 또한 기존 메서드에선 버튼이 다시 보이도록 하는 기능만 담당하도록 분리했습니다. |3페이지|1~2페이지| |---|---| |![](https://i.imgur.com/hpgMHll.png)|![](https://i.imgur.com/zt0E3or.png)| ## 2. 못먹는음식 화면의 UI 3번째 페이지(못먹는음식 화면)의 경우, 못먹는음식을 CollectionView로 구현했습니다. 향후 데이터가 추가되거나 필터링 기능을 추가할 것에 대비하여 데이터 변동에 유연한 `DiffableDataSource` 및 `Compositional Layout`을 활용했습니다. 못먹는음식 데이터를 관리하기 위한 Model 타입으로 `DislikedFoodCell`의 중첩타입으로 `DislikedFood`를 생성했습니다. `DislikedFood` 타입의 `ischecked` 프로퍼티 (Bool 타입)를 통해 사용자의 check 여부를 저장합니다. 해당 데이터를 관리하는 역할은 ViewModel이 담당하도록 했습니다. 못먹는음식 데이터의 이미지는 Asset에 저장하고, ViewModel에서 해당 이미지와 음식별 타이틀을 지정하여 데이터를 배열 타입으로 가지도록 했습니다. ## 3. 못먹는음식 화면의 Tap 이벤트 처리 사용자는 Cell을 Tap하여 못먹는음식을 다중 선택할 수 있습니다. 이때 `ViewController`에서 RxCocoa를 통해 selectedCell의 indexPath를 전달하고, `ViewModel`은 해당 Food의 isChecked를 toggle하고, 다시 해당 `Cell`의 체크박스 이미지와 배경색이 toggle 되도록 했습니다. 명확한 역할 분리를 위해 `ViewModel`은 Food 데이터를 관리하고, `Cell`은 View를 그리도록 했습니다. 따라서 Cell은 자신의 check 여부를 알 수 없습니다. # 테스트 방법 다음 PR에서 커버할 예정입니다. # 리뷰 노트 향후 UserDefaults를 사용하여 앱 최초실행 여부를 판단하고, 최소실행인 경우에만 OnboardingPage가 나타나도록 합니다. 또한 RealM 연동을 통해 싫어하는음식을 Local DB로 관리할 예정입니다. 각 버튼(Skip, 확인)의 기능 또한 다음 PR에서 구현 예정입니다. *디자이너 합류가 예정되어 OnboardingPage 등 화면 디자인은 추후 변경될 수 있습니다. # 스크린샷 - 영상 # [PR-2. RealM 연동하여 못먹는음식 데이터를 Local DB에 저장합니다.](https://github.com/just1103/WhatWeEat/pull/2) # 배경 `OnboardingPage`에서 사용자가 제출한 못먹는음식 데이터를 Loacl DB에 저장하기 위해 Realm을 사용했습니다. # 작업 내용 ## 1. Local DB 선택 ### Local DB 비교 아래와 같이 RealM, SQLite, CoreData 3개 DB를 비교한 결과, RealM을 채택했습니다. |DB|[Realm ✅](https://github.com/realm/realm-swift)|[SQLite](https://github.com/sqlite/sqlite)|[CoreData](https://developer.apple.com/documentation/coredata)| |-|-|-|-| |특징|- 모바일에 최적화된 *NoSQL DB 라이브러리로 가장 최근에 등장함</br>*NoSQL (Not Only SQL) : SQL 외 여러 유형의 DB를 사용함</br>- 데이터 객체가 Objective-C 클래스로 표현되며, 가볍고 반응형임</br>- *데이터 컨테이너 모델을 사용함</br>*컨테이너 : OS상의 논리적인 구획(컨테이너)을 만들고, 앱 실행에 필요한 라이브러리/앱을 하나로 모아, 별도 서버처럼 사용하도록 만든 구조. 컨테이너는 오버헤드가 적으므로 가볍고 속도가 빠름</br>- Swift, Objective-C, Java, Kotlin, C#, JavaScript 등의 SDK를 제공|- 전세계적으로 가장 많이 사용되는 SQL DB 라이브러리</br>- 전통적인 테이블 지향 관계형 DB</br>*ORM (Object Relational Mapping) : OOP의 “객체” 및 관계형 DB의 데이터인 “테이블”을 매핑하는 구조</br>- [단일 사용자의 데이터를 저장하는 로컬 앱에 적합](https://smoh.tistory.com/368)|- 앱의 Model layer를 관리하기 위한 Apple의 First-party FrameWork (DB가 아님) </br>- Persistence 기능은 SQLite에 의해 지원됨 </br>- ORM 모델을 추상화한 구조 |장점|- [SQLite 및 CoreData 대비 작업 속도가 빠름](https://hackernoon.com/sqlite-vs-realm-which-database-to-choose-in-2021-8g1v3wf9)</br>- 하나의 앱에서 여러 DB를 사용 가능</br>- 데이터를 객체 형태로 저장하여 DB에서 가져온 데이터를 앱에 즉시 사용 가능하고, CoreData 코드를 Realm으로 Migration하기 용이함</br>- 데이터 저장 용량이 무제한/무료</br>- iOS 및 Android 간의 DB 공유 가능</br>- Realm Studio를 통해 Finder로 DB 확인이 쉬움</br>- CoreData 대비 직관적인 코드</br>- DB 처리에 Main Thread를 사용하므로 안정적임 (장점이자 단점)|- C언어로 작성되어 가벼우며, 전체 DB를 디스크 파일 1개에 저장</br>- 설정이 쉬움</br>- Thread-safe</br>- 다양한 OS에서 사용 가능 (MacOS X, iOS, Android, Window, Linux)|- 데이터를 객체 (NSManagedObject) 형태로 저장하여 DB에서 가져온 데이터를 앱에 즉시 사용 가능 </br>- SQLite 대비 속도가 빠름 |단점|- Thread-confined함 (스레드별 객체 관리가 필요. 다른 스레드로 전달하려면 wrapper 등이 필요)</br>- SQLite 대비 바이너리 용량이 큼</br>- 다양한 query를 지원하지 않음|- 동시성 (Concurrency)에 제한이 있음 (여러 프로세스가 DB에 접근/quering이 가능하지만, 한 번에 1개 프로세스만 처리 가능)</br>- 데이터 용량이 큰 경우 부적합</br>- 접근 권한 종류가 한 가지 밖에 없음|- 사용이 직관적이지 않고 번거로움 (Entity 생성, 코드로 데이터를 Read/Write 등)</br>- 메모리 및 저장공간 소모가 큼 (In-memory 방식은 메모리에 로딩된 객체만 수정 가능)</br>- 데이터 중복을 방지하는 unique key 기능이 없음</br>- Thread-unsafe</br>- 오버헤드 발생 가능</br>- Android 등 크로스 플랫폼을 지원하지 않음| ### 기술스택 고려사항 1. 하위 버전 호환성에는 문제가 없는가? - Deployment Target을 iOS 14이상으로 설정할 계획이고, Xcode 버전 13.4을 사용 중이므로 문제 없습니다. - Realm : [SPM 사용 기준 iOS 11 이상](https://docs.mongodb.com/realm/sdk/swift/install/) 2. 안정적으로 운용 가능한가? - 모바일에 최적화되어 있으므로 iOS를 지속 지원할 것으로 예상되며, [2.0 버전부터 안정화](https://www.theteams.kr/teams/859/post/64925)됐다고 판단했습니다. 3. 미래 지속가능성이 있는가? - 지속적으로 기능이 업데이트되고 있습니다. - 서비스를 확장하여 Android 앱을 개발할 경우에도 사용할 수 있습니다. 4. 리스크를 최소화 할 수 있는가? 알고있는 리스크는 무엇인가? - First-party 라이브러리가 아니므로 서비스가 중단될 리스크가 있지만, 중단될 가능성이 낮다고 판단했습니다. - Realm은 Thread별 객체 관리가 필요하며, 바이너리 용량이 크므로 지속적으로 Thread/용량을 관리할 필요가 있습니다. 또한 현재 용량 무제한/무료이지만 가격 정책이 변경될 수 있습니다. 5. 어떤 의존성 관리도구를 사용하여 관리할 수 있는가? - Cocoa Pods, Carthage, SPM을 사용할 수 있습니다. - First-party 라이브러리인 SPM을 사용중이므로 적합합니다. ## 2. RealM 연동 싱글톤 패턴을 적용하여 RealmManager 타입을 생성하고, realm 객체를 가지도록 했습니다. 또한 RealM 데이터 저장을 위한 객체 타입으로 `DislikedFoodForRealM` 타입을 추가했습니다. `OnboardingPage`의 못먹는음식 화면에서 `확인 버튼`을 Tap하는 이벤트를 받을 때마다, RealM에 못먹는음식 데이터를 업데이트 (기존 데이터 전체삭제, 새로운 데이터 추가)하도록 구현했습니다. ## 3. Skip 버튼 구현 1, 2 페이지에서 Skip 버튼을 Tap한 경우, 바로 못먹는음식 페이지로 이동하도록 했습니다. `UIPageViewController`의 `setViewControllers` 메서드를 활용하여 못먹는음식 페이지로 이동하도록 했고, 이 경우 기존 `pageControl`과 `Skip` 버튼이 사라지도록 했습니다. # 테스트 방법 `RealM Studio` 맥앱을 활용해 RealM 데이터가 정상적으로 저장되는지 확인했습니다. 데이터 저장 경로의 경우 다음과 같은 방법으로 찾았습니다. ```swift Realm.Configuration.defaultConfiguration.fileURL ``` - RealM Studio에 저장된 데이터 확인 # 리뷰 노트 - UserDefault를 통해 앱을 처음 실행하는 것이 아니라면 바로 메인 메뉴로 이동할 수 있도록 구현 # 스크린샷 # [PR-3. 앱 최초실행 시 OnboardingPage, 그 이후에는 MainTabBarController의 Home 화면을 보여줍니다.](https://github.com/just1103/WhatWeEat/pull/3) # 배경 앱을 처음 설치하여 실행한 경우에만 OnboardingPage를 보여주고 이후에는 Home 화면을 바로 보여주도록 했습니다. 앱을 실행할 때마다 OnboardingPage를 보여주는 것은 비효율적이라 판단했고, 대부분의 상용 앱도 이런 형태롤 갖추고 있어 해당 방식을 택했습니다. # 작업 내용 ## 1. MainTabBarController를 통한 Tab Bar 구현 `생성자 주입`을 통해 `Tab Bar`에 반영할 화면정보를 전달했습니다. Home 화면에서 `Tab Bar`와 `Navigation Bar`가 모두 존재합니다. `MainTabBarController`를 초기화하는 과정에서 일부 프로퍼티가 `viewDidLoad` 호출 이후에 초기화되는 문제가 발생했습니다. 확인 결과, `UITabBarController` 이니셜라이저 내부에 `super.init`이 호출되면서 비정상적인 side-effect가 발생하는 것이 원인이었습니다. 따라서 일반적으로 `viewDidLoad`에 배치했던 메서드를 부득이 `viewWillAppear`에서 호출하여 문제를 해결했습니다. ## 2. UserDefault를 활용하여 앱이 처음 실행되었는지 확인 `FirstLaunchChecker`를 생성하여 UserDefault가 `"isFirstLaunched"`를 키로 값을 가지고 있는지에 따라 Bool 타입을 반환하도록 구현했습니다. 이를 통해 첫 실행일 경우 OnboardingPage를, 첫 실행이 아니면 `MainTabBarController`를 보여주도록 했습니다. ## 3. FlowCoordinator를 통한 MVVM-C 구현 `FlowCoordinator`를 통해 의존성 주입을 관리하고, 화면전환 역할을 담당하도록 했습니다. `생성자 주입`을 통해 `navigationController`를 주입받고, 화면전환 시 해당 `navigationController`가 다음 화면을 push 하도록 했습니다. 이때 화면전환 관련 정보는 `ViewModel`이 알고 있는 것이 적절하다고 판단했습니다. 따라서 화면전환 동작을 클로저 타입으로 `actions`에 저장하고, actions를 `ViewModel`의 생성자 주입으로 전달했습니다. # 테스트 방법 - Simulator를 통해 테스트를 진행합니다. 앱 실행 이후, 앱을 삭제하고 다시 빌드하면 OnboardingPage가 다시 나타나는 것을 확인 가능합니다. # 리뷰 노트 - `함께 메뉴 결정`, `혼밥 메뉴 결정` 탭을 눌렀을 때 나오는 페이지 구현 예정입니다. # 스크린샷 # [PR-4. 함께메뉴결정 탭 및 혼밥메뉴결정 탭의 미니게임 준비 화면을 보여줍니다.](https://github.com/just1103/WhatWeEat/pull/4) # 배경 - 본격적으로 미니게임을 시작하기 전에 사용자가 게임을 준비할 수 있도록 유도합니다. - `함께메뉴결정` 탭의 경우, 사용자들을 그룹핑하여 추후 미니게임 결과를 합산하게 됩니다. `그룹만들기` 버튼을 탭한 사용자가 해당 그룹의 Host가 되고, 공유하기 버튼을 통해 ActivityView를 띄우고 팀원들에게 간편하게 PIN 번호를 공유할 수 있습니다. Host가 아닌 팀원들은 `PIN으로 입장하기` 버튼을 탭하여 PIN 번호를 입력하고 그룹에 입장할 수 있습니다. - `혼밥메뉴결정` 탭의 경우, 미니게임을 혼자서 진행하므로 위 과정이 필요 없습니다. `미니게임 시작` 버튼을 누르면 게임이 시작됩니다. # 작업 내용 ## 1. `혼밥메뉴결정`, `함께메뉴결정` 탭 추가 flowCoordinator를 통해 각 탭에 해당하는 ViewController를 생성한 후 `MainTabBarController`의 pages로 주입시켰습니다. ## 2. ActivityView 내용 커스텀 ActivityView를 통해 PIN 번호를 공유할 때 Title과 Content를 커스텀하기 위해 `UIActivityItemSource`를 준수하는 `SharePinNumberActivityItemSource`를 구현했습니다. - activityViewControllerPlaceholderItem(:) : 데이터의 PlaceHolder 객체를 반환 (필수 구현) - activityViewController(:itemForActivityType:) : 작업을 수행할 데이터 객체를 반환 (필수 구현) - activityViewController(:subjectForActivityType:) : Activity의 제목을 지원하는 경우, 이를 반환 - activityViewControllerLinkMetadata(:) : MetaData를 통해 ActivityView에 띄울 컨텐츠를 지정 후 이를 반환 ActivityView를 화면에 띄울 때에는, Rx를 활용하여 `공유하기` 버튼을 눌렀을 경우 화면에 present할 수 있도록 했습니다. 또한 아이패드의 지원을 위해 다음과 같은 코드를 추가했습니다. ```swift activityViewController.popoverPresentationController?.sourceView = self?.shareButton activityViewController.popoverPresentationController?.permittedArrowDirections = .down ``` ActivityView의 Title은 Host (PIN 번호를 공유하는 주체)가 확인할 수 있도록 "[우리뭐먹지] 팀원과 PIN 번호를 공유해보세요" 메세지를 나타내고, PIN 번호를 공유받는 팀원은 해당 앱을 실행하여 그룹에 입장할 수 있도록 "[우리뭐먹지] 팀원이 공유한 PIN 번호: 1111 / PIN 번호를 통해 입장하여 오늘의 메뉴를 골라보세요" 메세지를 나타냈습니다. ## 3. NavigationBar의 색상 변경 NavigationBar의 색상을 앱의 메인 색깔로 변경해주고자 했습니다. 초기에는 NavigationBar의 `backgroundColor`를 지정했을 때, 상단의 StatusBar 부분에는 색상이 적용되지 않는 문제가 발생했습니다. 따라서 View의 backgroundColor를 `ColorPalette.mainYellow`로 변경해주고, SubView인 StackView의 backgroundColor를 white로 설정하여 문제를 해결했습니다. # 테스트 방법 - 실기기 테스트를 통해 ActivityView에서 다른 앱으로 공유하기 기능을 사용하고, 공유되는 텍스트를 확인합니다. # 리뷰 노트 - ActivityView의 아이콘은 추후 앱 아이콘으로 변경할 예정입니다. - 처음에는 PIN 번호 텍스트를 공유하는 것보다는, PIN 번호가 담긴 URL을 공유하여 팀원들이 해당 링크를 탭하면 `즉시 앱을 실행하고 해당 그룹에 입장하도록 하는 기능`이 UX 측면에서 유리할 것으로 판단했습니다. 하지만 앱을 다운받았으나 아직 실행하지 않았던 사용자는 `OnboardingPage`를 확인할 수 없고, `못먹는음식`을 선택할 수 없다는 단점이 있어서 위 기능을 구현하지 않았습니다. 추후 보완이 가능하다면 위 기능을 구현하도록 변경할 수 있습니다. - 레이아웃의 경우 추후 디자인이 변경될 것을 고려하여 형태만 잡아놓았습니다. # 스크린샷 # [PR-5. 네트워크를 구현하여 홈 탭 및 함께메뉴결정 탭에 서버 데이터를 반영합니다.](https://github.com/just1103/WhatWeEat/pull/5) # 배경 서버와 통신을 해서 홈탭의 랜덤 메뉴, 함께메뉴결정의 PIN 번호를 띄울 수 있도록 했습니다. # 작업 내용 ## 1. 네트워크 구현 및 API 추상화 `RxSwift`를 활용하여 비동기 작업을 처리했습니다. 서버에서 받아온 데이터는 `Observable` 타입으로 반환하고, ViewModel에서 ViewController에 전달하여 화면에 나타내는 구조로 구현했습니다. API를 열거형으로 관리하는 경우, API를 추가할 때마다 새로운 case를 생성하여 열거형이 비대해지고, 열거형 관련 switch문을 매번 수정해야 하는 번거로움이 있었습니다. 따라서 API마다 독립적인 구조체 타입으로 관리되도록 변경하고, URL 프로퍼티 외에도 HttpMethod 프로퍼티를 추가한 APIProtocol 타입을 채택하도록 개선했습니다. 이로써 코드유지 보수가 용이하며, 협업 시 각자 담당한 API 구조체 타입만 관리하면 되기 때문에 충돌을 방지할 수 있습니다. ## 2. 데모 웹 애플리케이션을 통한 네트워크 테스트 `jar 실행파일`을 설치하여 `데모 웹 애플리케이션`을 통한 네트워크 테스트 (baseURL을 http://localhost:8080/로 설정)를 구현했습니다. 실제 서버와 독립적인 테스트를 실행한 이유는 아래와 같습니다. - 실제 서버와 통신할 경우 테스트의 속도가 느려짐 - 인터넷 연결상태에 따라 테스트 결과가 달라지므로 테스트 신뢰도가 떨어짐 - 실제 서버와 통신을 하며 서버에 테스트 데이터가 불필요하게 업로드되는 Side-Effect가 발생함 ## 3. 홈탭의 랜덤메뉴 랜덤메뉴의 이름, 이미지를 서버에서 받아 띄워줬습니다. `HomeViewController`의 `viewDidLoad` 시점에서 서버로부터 데이터를 받아오도록 했습니다. 이때 서버에서 보내주는 데이터 형식이 JSON이기 때문에, `Menu` Entity를 추가했습니다. 추후 GameResult에 대한 데이터를 Get할 때에는 각 메뉴에 해당하는 키워드를 보여줘야 했기에 이는 옵셔널 타입으로 뒀습니다. ## 4. 함께메뉴결정 탭의 PIN 번호 `TogetherMenuViewController`의 `makeGroupButton`을 탭할 경우 `TogetherMenuViewModel`에서 서버에게 PIN 번호를 요청합니다. `request(api:)` 메서드를 통해 `Observable<Int>`로 반환 값을 받으면 이를 FlowCoordinator의 `showSharePinNumberPage`로 전달 후 `SharePinNumberPageViewModel`로 전달하여 `SharePinNumberPageViewController`에서 View에 띄울 수 있도록 했습니다. 서버에서 JSON 형태로 데이터를 전달해주는 것이 아니라, Int 타입으로 PIN 번호만 보내주기 때문에 따로 Entity를 구현하진 않았습니다. # 테스트 방법 데모 웹 애플리케이션을 로컬에 설치하여 서버 데이터가 정상적으로 반영되는지 확인합니다. # 리뷰 노트 - 실제 Remote 서버 연결이 완료된 후 실기기 테스트를 통해 이미지가 너무 늦게 뜨는 경우 `Activity Indicator`를 추가할 예정입니다. # 스크린샷 <img src="https://i.imgur.com/RPeN0ci.gif" width="250"> # PR-6. Main 화면의 네이게이션바 우상단의 설정 버튼을 탭하면 설정 화면이 나타납니다. # 배경 사용자가 못먹는 음식 리스트를 수정하고, App에 대한 다양한 정보를 확인할 수 있도록 하기 위해 `설정` 화면을 구현했습니다. `탭바`를 사용하는 대부분의 상용앱은 `마이페이지 탭`이 있는데, 우리뭐먹지 앱 기능상 사용자 개인정보를 처리하지 않으므로 마이페이지가 없고, 설정 화면에서는 부차적인 기능만 수행하므로 별도 탭을 만들지 않고 네비게이션바에서 접근하도록 구현했습니다. # 작업 내용 ## 1. UITableView를 통한 설정 화면 구현 설정화면의 경우 항상 List로 구성이 된다고 판단했습니다. 따라서 `CollectionView`가 아닌 `TableView`를 활용해 설정 페이지를 만들었습니다. Version Update를 위한 Cell은 구성이 달랐기에 `VersionCell`과 일반적인 `SettingCell`을 구분했습니다. 또한 설정의 목록은 고정적이므로 Diffable DataSource를 사용하지 않았습니다. ## 2. 3개 Section으로 구분하고, 2개의 Cell Type을 활용 Section은 `dislikedFood`, `ordinary`, `version` 3가지로 구분했습니다. 이처럼 TableView에 여러 개의 Section이 있는 경우 viewModelData.bind(to: tableView.rx.items) 형태로 binding이 불가능하여, UITableViewDataSource 메서드를 사용했습니다. 또한 설정 화면에 필요한 Item의 경우 `SettingItem` 프로토콜을 준수하는 `OrdinarySettingItem`과 `VersionSettingItem`을 두어 `SettingViewModel`이 가지고 있도록 했습니다. 이후 rx를 통해 SettingItem을 전달받아, UITableViewDataSource 메서드를 사용하여 TableView를 구성했습니다. ## 3. Delegate 패턴으로 버전정보의 `업데이트` 버튼의 탭 이벤트를 처리 사용자의 버전정보와 앱의 최신버전을 비교하여 업데이트가 가능한 경우 버튼 타이틀을 `업데이트`로, 업데이트가 불가한 경우 `최신버전`으로 표시했습니다. 이때 Cell의 delegate를 ViewController로 설정하여 탭 이벤트를 처리하도록 구현했습니다. 향후 AppStore 앱으로 이동시키도록 구현할 예정입니다. ## 4. `못먹는음식 수정하기` 화면을 띄울 때 realm 데이터 반영 OnbarodingPage에서 표시한 못먹는 음식을 `설정`에서 수정할 수 있는 기능을 구현했습니다. 코드 재사용을 위해 기존의 `configureDislikedFoods` 메서드를 리팩토링하여 못먹는음식 정보를 표시할 때, 먼저 realm 데이터를 받아와서 반영하도록 했습니다. 또한 `RealmManager` 타입에 CRUD 메서드 일부를 추가하여 ViewModel에서 이를 호출하도록 했습니다. 이외에도 UX를 고려하여 OnbarodingPage에서 못먹는음식 화면의 확인 버튼을 탭해야 앱의 최초실행 여부를 나타내는 `isFirstLaunched`가 false가 되도록 기능을 변경했습니다. ## 5. '못먹는 음식 수정하기', '버전정보'를 제외한 다른 리스트에 대한 DetailView 구현 설정의 다른 리스트들의 경우 Text만 올라가면 됐기 때문에, 셀을 선택하면 `TextView`를 가지고 있는 `SettingDetailViewController`를 띄워주도록 했습니다. 사용자가 수정할 수 없도록 `isEditable` 프로퍼티를 `false`로 했으며, `TextView`가 `SettingDetailViewController`의 View에 가득차도록 했습니다. # 테스트 방법 버전 정보확인의 경우 추후 `ViewModel UnitTest`를 통해 검증할 예정입니다. (이미 시뮬레이터를 통해 정상 동작하는 것은 확인했습니다.) # 리뷰 노트 - 향후 버전정보의 `업데이트` 버튼을 탭하면, AppStore 앱으로 이동시켜 사용자가 앱을 업데이트할 수 있도록 구현할 예정입니다. - 향후 `친구에게 추천하기` 버튼을 탭하면, 앱 다운로드 링크를 복사하거나, SNS 공유 등을 통해 링크를 전달하는 기능을 구현할 예정입니다. - 향후 개인정보 처리방침, 오픈소스 라이센스, 개발자에게 피드백하기의 경우 Text를 수정할 예정입니다. # 스크린샷 <img src="https://user-images.githubusercontent.com/70856586/172523548-f3092c8c-fd3e-429a-a2dd-479b831672de.gif" width="250"> # PR-7. Game 화면 및 Game 결과대기 화면 구현 # 배경 사용자가 Game을 통해 9가지 질문에 대답하는 기능을 구현했습니다. 질문은 Card 형태로 띄우고, 7개 질문은 버튼을 통한 `좋아요/싫어요`, 2개 질문은 CollectionView를 통한 `다중선택`로 답변하도록 했습니다. 이때 사용자가 Game에 집중할 수 있도록 NavigationBar 및 TabBar가 숨겨지도록 했습니다. # 작업 내용 ## 1. 전체적인 게임 애니메이션 구현 <참고한 게임 애니메이션> ![](https://i.imgur.com/LDTWedi.png) 위 애니메이션처럼 `답변 버튼`을 탭하면 답변 종류 (좋아요, 싫어요, 상관 없음)에 따라 카드가 날아가고, `이전 질문 버튼`을 탭하면 카드가 날아간 방향에서 다시 돌아오도록 구현했습니다. 카드는 별도의 Custom View 타입을 생성하여 구현했고, UI요소인 만큼 `CardGameViewController`가 가지고 있도록 했으며, 애니메이션을 위해 `CGRect`로 위치를 잡았습니다. 따라서 `translatesAutoresizingMaskIntoConstraints`를 true로 줬습니다. 카드 위치의 경우 화면에 보이는 1, 2, 3번째 카드의 위치를 고정해두었고, 답변을 제출하거나, 이전 질문으로 되돌렸을 때 특정 카드의 위치를 바꿔주도록 했습니다. 카드가 제출되어 날라가는 경우 `CGAffineTransform(translateX:y)`와 `CGAffineTransform(rotationAngle:)`을 사용했고, 카드가 다시 돌아오는 경우 날라갔던 위치를 기억하고 있었기 때문에 `CGAffineTransform(rotationAngle:)`만 사용해서 다시 원래대로 돌아오도록 했습니다. ## 2. TabBar마다 개별적인 Coordinator 및 NavigationController를 가지도록 구현 HIG 문서의 `Tab bars` 내용 (They also let people quickly switch between sections of the view while preserving the current navigation state within each section.)과 같이 TabBar 마다 독립적으로 화면이 작동하도록 해야 하므로 TabBarViewController를 띄우는 `Coordinator` 및 `NavigationController` (이하 부모 Coordinator 및 Navigation), 그리고 특정 TabBar 내부에서 화면을 이동하는 `Coordinator` 및 `NavigationController` (이하 자식 Coordinator 및 Navigation)을 분리했습니다. 또한 부모 Coordinator 및 자식 Coordinator는 `Delegate Pattern`을 활용하여 소통하도록 했습니다. Coordinator 구조는 아래와 같습니다. ![](https://i.imgur.com/OiD4Qvs.png) 사용자가 게임을 할 때 게임에 몰입할 수 있도록 Game 화면에서는 `부모 Coordinator`를 통해 NavigationBar 및 TabBar가 숨겨지도록 했고, 게임 대기/결과 화면에서 다시 보여지도록 구현했습니다. 또한 `자식 Coordinator`에는 Game 화면에 필요한 별도의 Coordinator를 추가하여 코드 재사용성을 개선했습니다. ## 3. 다중선택 게임의 CollectionView 구현 `다중선택 게임`의 경우 선택지가 고정적이고 Animation이 따로 필요하지 않았기에 `DiffableDataSource`를 사용하지 않았습니다. 대신 `ComposiontalLayout`을 통해 CollectionView의 레이아웃을 잡고, Rx의 `bind` 오퍼레이터를 통해 데이터를 넣어줬습니다. 다중선택 게임의 경우 기존에는 다른 형태의 View로 넘어가서 게임을 진행하는 방식이었으나, 기존 `Yes/No 게임`처럼 카드 형태로 되어 있을 경우 게임이 얼마나 남았는지 시각적으로 파악할 수 있기 때문에 다중선택 게임 또한 카드 방식으로 구현했습니다. ## 4. 결과 제출 버튼을 누르면 서버에 결과를 취합하여 전송하도록 구현 `혼자메뉴결정`, `함께메뉴결정` 모두 마지막 질문에서 다음 버튼을 누르는 경우, 게임 답변을 서버에 전송하도록 구현했습니다. 결과 전송 API의 경우 PIN번호가 있으면 `함께메뉴결정`, 없으면 `혼자메뉴결정`에 해당하는 URL로 데이터를 Post 도록 했으며, `httpMethod`가 Post인 경우 `httpBody`에 인코딩한 JSON 데이터를 넣도록 했습니다. ## 5. `함께메뉴결정 탭`의 `Game 결과대기 화면` 구현 10초 Timer의 타이머를 통해 서버에서 제출인원수 (SubmissionCount)를 10초 간격으로 받아 화면에 나타내도록 했습니다. 따라서 얼마나 많은 인원이 제출을 했는지 확인이 가능하도록 했습니다. Refresh 버튼을 만드는 방안도 고려했으나 이는 사용자가 계속해서 해당 버튼을 눌러줘야 하는 불편함이 있기에, 자동으로 갱신되도록 구현했습니다. ## 6. `게임 다시 시작 버튼` 구현 `함께메뉴결정 탭`의 `Game 결과대기 화면`에서 `게임 다시 시작 버튼`을 탭하면, 서버에 제출한 개인 데이터를 삭제하고, 해당 탭의 초기화면으로 돌아가도록 구현했습니다. 초기화면으로 돌아갈 때는 메모리 관리를 위해 `부모 Coordinator`의 `childCoordinators` 프로퍼티에서 기존의 `자식 Coordinator`를 삭제하고, 새롭게 생성한 `자식 Coordinator`를 추가하도록 했습니다. - 이때 핀넘버 공유 (SharePinNumber) 화면 및 게임결과 대기 (Submission) 화면의 ViewModel이 메모리에서 해제되지 않는 문제가 발생했습니다. 원인을 파악하지 못하여 추후 리팩토링하면서 해결할 예정입니다. # 테스트 방법 - Simulator 화면 확인으로 대체했습니다. (ViewModel 테스트코드 추후 추가 예정) # 리뷰 노트 - 디자인은 추후 변경될 수 있습니다. # 스크린샷 ![](https://i.imgur.com/T8Gv6PA.gif) # PR-8. 결과확인하기 버튼을 탭하면 게임결과 화면을 보여줍니다. # 배경 - 팀원들/개인이 제출한 게임답변을 서버에 전달하고, 추천메뉴를 받아 보여주는 화면입니다. - 총 3개 메뉴를 확인할 수 있고, 추천메뉴에 대해 다수의 팀원들이 원하는 키워드를 차례로 보여줍니다. # 작업 내용 - Host가 `결과확인하기` 버튼을 탭하면, 팀원들의 화면에서 hidden 처리되었던 버튼을 보이도록 변경하여 팀원들도 추천메뉴를 확인할 수 있도록 했습니다. - 결과대기 화면의 로직을 일부 수정했습니다. - 결과대기 화면에서 앱을 종료한 경우 재접속하면 해당 화면을 자동으로 보여주는 등 다양한 사용자 시나리오를 고려했습니다. - 결과대기 화면에서 `게임다시시작하기` 버튼을 탭한 경우, 해당 사용자의 제출 데이터를 삭제하도록 했습니다. - Firebase를 통해 기기별 토큰을 받아 호스트를 서버에서 알 수 있도록 했습니다. - 네트워크 연결상태를 확인하고, 인터넷이 불안정한 경우 사용자에게 알리도록 했습니다. - Lottie를 통해 애니메이션을 추가했습니다. ## 1. 최종메뉴 처리 개인/팀 게임답변을 서버에서 POST 형태로 제출하고, 게임결과로 3개 메뉴와 총 참여인원수를 받아서 나타냈습니다. 사용자의 의사결정을 돕기 위해 `다음메뉴보기` 횟수를 3번으로 제한하였고, `다음메뉴보기` 버튼을 탭하면 ViewModel로부터 다음 메뉴 정보를 받아와서 ViewController에서 메뉴이름과 해당 메뉴의 키워드를 수정하도록 했습니다. 서버의 효율성을 높이기 위해 서버개발자와 협의하여 게임결과를 제출할 때 (`ResultSubmissionAPI`, POST 메서드) `함께메뉴결정 탭`이라면 response로 nil을 받고, `혼밥메뉴결정 탭`이라면 게임결과를 받도록 구분했습니다. 이를 통해 혼밥메뉴결정은 별도의 게임결과 요청 API가 필요하지 않게 되었습니다. ## 2. 결과대기 화면에서 앱을 종료한 경우 재접속하면 해당 화면을 자동으로 띄우도록 구현 결과대기 화면에서 앱을 종료한 경우 `TogetherGameSubmittedChecker`를 통해 답변제출 여부와 가장 최근의 PIN Number를 `UserDefault`에 저장하도록 구현했습니다. 단순한 Bool타입과 Int타입인 만큼 UserDefault에 저장해도 문제가 없다고 판단했습니다. 게임을 제출했을 때 `isTogetherGameSubmitted`를 true로 바꾸고, `게임 시작하기` 버튼을 눌렀을 때 / `게임 다시 시작` 버튼을 눌렀을 때, `결과 확인` 버튼을 눌렀을 때에는 false로 바꾸도록 했습니다. 앱을 종료 후 재실행했을 때에는 `MainTabBarCoordinator`에서 `isSubmitted`를 확인 후 true면 가장 최근에 제출했던 대기 화면이 보여지도록 했습니다. ```swift func start() { makeMainTabBarPage() if TogetherGameSubmittedChecker.isSubmitted { guard let togetherCoordinator = childCoordinators.filter { $0.type == .togetherMenu }.first as? TogetherMenuCoordinator else { return } togetherCoordinator.showLatestSubmissionPage(pinNumber: TogetherGameSubmittedChecker.latestPinNumber) } } ``` ## 3. Firebase를 통해 기기별 토큰을 받아 Host를 서버에서 알 수 있도록 구현 Group으로 게임을 생성할 때 사용자의 토큰을 통해 서버에서 게임을 만든 `Host`가 누구인지 알 수 있도록 해야 했습니다. 따라서 FireBase를 연동했고 그 중 `FirebaseMessaging`을 사용했습니다. 토큰은 메모리에 올라가는 경우 앱이 종료될 때까지 메모리에서 해제되지 않도록 타입 프로퍼티로 선언했습니다. 토큰은 아래 메서드를 통해 받았습니다. ```swift extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { guard let fcmToken = fcmToken else { return } AppDelegate.token = fcmToken } } ``` 1. `PIN Number 생성하기 버튼`을 탭한 사용자를 서버에서 Host로 인식하도록 처리했습니다. 2. 결과대기 화면에서 호스트의 토큰과 기기의 토큰이 동일한지 비교하여 Bool 타입을 받도록 합니다. 3. 서버에서 받은 Bool 타입이 True인 경우 Host이므로 `결과확인하기` 버튼이 보이도록 합니다. 4. False인 경우 Host가 `결과확인하기` 버튼을 누를 때까지 버튼을 Hidden 처리합니다. ## 4. 네트워크 연결상태 확인 `Network`를 import하고, `NetworkConnectionManager` 타입을 싱글톤으로 구현했습니다. 각 화면에서 네트워크 연결상태를 확인하고, 이상이 있는 경우 오류화면을 나타내어 사용자가 인터넷에 재접속하도록 안내했습니다. 오류 화면에서 `refresh` 버튼을 누르면, 네트워크 연결 상태에 따라 다시 오류 화면을 보여주거나, Data를 Fetch하여 정상 화면이 보이도록 했습니다. 오류화면을 pop하면서 정상 화면을 보여주기 때문에 `viewWillAppear`를 사용해 화면이 다시 보이는 경우 데이터를 fetch해서 화면에 띄울 수 있도록 했습니다. ## 5. Lottie를 통해 애니메이션을 추가 `Lottie` 라이브러리를 Package에 추가하여 JSON 파일을 애니메이션으로 나타낼 수 있도록 했습니다. 게임시작준비 화면에서 Circle 애니메이션을 사용했고, 게임결과대기 화면에서 Loading 애니메이션을 활용하여 UX를 개선했습니다. 애니메이션을 위한 JSON 파일은 Asset을 통해 관리하고 있습니다. # 테스트 방법 - 직접 서버에 연결하여 테스트 결과가 제대로 나오는지 확인했습니다. - 실 기기에 연결해 Network 연결을 끄고 켜며 네트워크 연결 상태에 따라 적합한 View가 보이는지 확인했습니다. # 리뷰 노트 - 앱출시 목표일정을 고려하여 주변 식당을 보여주는 기능 (지도 SDK), Notification 알림을 띄우는 기능 (Firebase)은 다음 배포 버전에서 추가하기로 했습니다. # 스크린샷 |결과대기 화면에서 앱 종료/재접속|혼밥메뉴결정 탭에서 추천메뉴 확인|함께메뉴결정 탭에서 팀원이 메뉴 확인| |-|-|-| |<img src="https://user-images.githubusercontent.com/70856586/175904878-5bb3403a-9a22-44d7-9972-8521a75ee049.gif" width="200">|<img src="https://user-images.githubusercontent.com/70856586/175905149-1f9a8475-c602-4c0f-a73e-59eef7678420.gif" width="200">|<img src="https://user-images.githubusercontent.com/70856586/175905119-37f83189-dac5-4bb2-9e2e-e498e49b4aa7.gif" width="200">| # PR-9. 제목 # 배경 # 작업 내용 ## 1. ## 2. # 테스트 방법 # 리뷰 노트 # 스크린샷 # PR-9. 제목 # 배경 # 작업 내용 ## 1. ## 2. # 테스트 방법 # 리뷰 노트 # 스크린샷