자신이 기르는 희귀애완동물의 관리 기록을 남기고 볼 수 있는 애완동물 케어앱
- 개체 상세화면 전체 이미지 페이징 기능 추가
- 전체 이미지 페이징시 개체 상세화면 이미지 같은 이미지가 되도록 바인딩
- 펫 리스트 화면 그리드/테이블 모드 추가
- 펫 상세화면에서 펫 이미지 가지고 오지 못할 시 디폴트 이미지 출력
- 펫 리스트 화면 OperationQueue를 이용한 성능 개선
- 자연스러운 페이징 기능 추가
- 총 개발 기간: 2023년 9월 25일 ~ 2023년 11월 1일 (38일)
- 기획 및 디자인: 2023년 9월 25 ~ 2023년 10월 1일 (7일)
- 기능 개발 및 구현: 2023년 10월 1일 ~ 2023년 10월 30일
- 버그 수정 및 프로젝트 출시: 2023년 10월 31일 ~ 2023년 11월 1일
회차 | 내용 | 상세내용 | 예상 시간 | 실제 시간 | 기간 |
---|---|---|---|---|---|
이터레이션2 | ~ 2023/10/01 | ||||
추석 연휴 | 28 | ||||
추석 연휴 | 29 | ||||
프로젝트 생성 및 세팅, 필요 라이브러리 설치 | 1h | 1h | 30 | ||
DB | 애완동물 프로필 DB 테이블 구현 | 1h | 2h | 30 | |
도메인 | 펫 리스트 가져오는 UseCase, Repository 구현 및 PetEntity 구현 | 2h | 3h | 1 | |
PetListView | 펫 리스트 UI 및 레이아웃 구현 | 2h | 2h | 1 | |
이터레이션3 | ~ 2023/10/04 | ||||
PetListView | 콜렉션뷰 compositional layout으로 수정했다가 원상복귀 | 2h | 2h | 2 | |
PetListView | 상단 검색바 ,개체 추가 버튼 구현 | 1h | 30m | 2 | |
PetListView | ViewController와 View 분리 | 1h | 1h | 2 | |
PetListView | ViewModel 구현 | 1h | 1h | 2 | |
PetListView | 펫 리스트 화면, 레이아웃 오류 수정 | 30m | 30m | 3 | |
PetListView | 펫 등록 화면 기초 UI 구성 | 2h | 4h | 4 | |
이터레이션4 | ~ 2023/10/08 | ||||
DB | 종 DB 구현 | 2h | 2h | 5 | |
종 선택 화면 | 종 선택 화면 기본 UI 구현 | 2h | 2h | 5 | |
도메인 | 종을 가져오는 로직 구현 | 2h | 10h | 6 | |
DB | 종 DB에 id값 추가, Entity에 id값 추가 | 1h | 1h | 7 | |
DB | 저장된 종 가져오는 Stroage 구현 및 | 1h | 1h | 8 | |
이터레이션5 | ~ 2023/10/11 | ||||
SelectSpeciesView | 종 선택시 하위 종 가져오는 기능 구현 | 2h | 4h | 9 | |
SelectSpeciesView | 종선택 섹션 마지막 셀 등록셀로 구현 | 1h | 2h | 9 | |
SelectSpeciesView | ViewModel 바인딩, 선택된 종이 빈 하위 종을 가져올 경우 등록셀이 안나오던 버그 수정 | 2h | 2h | 9 | |
RegisterPetView | 텍스트필드 툴바 구현 | 1h | 1h | 10 | |
Presentation 계층에서 사용할 Model 구현 | 1h | 1h 30m | 10 | ||
RegisterPetView | 입양일, 해칭일 날짜 선택시 DatePicker ActionSheet 나오게 구현 | 2h | 1h 30m | 10 | |
RegisterPetView | ViewModel 구현 및 바인딩 | 2h | 2h | 11 | |
RegisterPetView | 상위 펫 선택시, 선택되었던 하위 종은 사라지는 기능 구현 | 2h | 4h | 11 | |
이터레이션6 | ~ 2023/10/15 | ||||
SelectSpeciesView | 종 추가 셀 클릭 시 textField Alert 나오게 설정 | 1h | 1h | 12 | |
RegisterPetView | 종 선택화면에서 적용 버튼 누를 시, 펫 등록 화면에서 선택된 종 표시 | 1h | 1h | 12 | |
SelectSpeciesView | 종 추가시 추가한 종 바로 가져오지 못하던 버그 수정 | 1h | 2h | 12 | |
RegisterPetView | 카메라, 앨범에서 이미지 등록 구현 | 1h | 1h | 14 | |
RegisterPetView | 펫 등록시 이미지 다운샘플링 및 FileManager로 저장 | 1h | 3h | 14 | |
PetListView | 펫 리스트 화면에서 저장된 사진이 있다면 가장 첫번째 사진이 나오도록, 사진이 없는 경우 종 이미지를 대신 보여주도록 구현 | 1h | 1h | 15 | |
PetListView | 펫 리스트 화면에서 종 선택시 종 필터링 | 1h | 1h | 15 | |
이터레이션7 | ~ 2023/10/18 | ||||
DetailPetView | 프로필 화면 탭바 NestedScrollView 구현 | 2h | 30h | 16 | |
DetailPetView | 펫 캘린더 ViewController 구현 | 2h | 10h | 16 | |
DetailPetView | 팻 캘린더 로직 구현 | 2h | 6h | 17 | |
DetailPetView | 펫 캘린더 ViewController 작업 셀 클릭시 캘린더에 이벤트 추가 및 하단 타임라인데 추가 | 2h | 3h | 17 | |
DetailPetView | 펫 상세화면 header ViewController 구현 | 2h | 5h | 18 | |
이터레이션8 | ~ 2023/10/22 | ||||
DetailPetView | 펫 무게 차트 구현 | 2h | 2h | 20 | |
DetailPetView | 펫 무게 ViewController 구현 | 2h | 3h | 20 | |
DetailPetView | 펫 무게 화면에서 무게 추가시 차트에 추가 | 2h | 3h | 21 | |
DetailPetView | 펫 무게 화면에서 삭제 기능 구현 | 2h | 2h | 21 | |
DetailPetView | 펫 캘린더 화면 버그 수정 | 2h | 10h | 22 | |
이터레이션9 | ~ 2023/10/25 | ||||
Calendar객체가 필요할때마다 생성했던 코드 제거 | 1h | 1h | 23 | ||
RegisterPetView | 이미지 저장시 이미지가 회전되던 버그 수정 | 1h | 7h | 23 | |
PetListView | 검색 기능 구현 | 2h | 1h | 24 | |
PetListView | 필터링 기능 구현 | 4h | 3h | 24 | |
SelectSpeciesView | 종 선택화면 UI 변경 | 1h | 1h | 24 | |
이터레이션10 | ~ 2023/10/30 | ||||
DetailPetView | 펫 삭제 기능 구현 | 2h | 1h | 25 | |
DetailPetView | 펫 정보 수정버튼 클릭시, 현재 펫 정보 수정 화면에 바인딩 | 2h | 1h | 26 | |
SelectSpeciesView | 펫 등록화면에서 기존에 선택된 종이 있다면, 종이 선택된 채로 나오는 기능 구현 | 1h | 5h | 26 | |
DetailPetView | 펫 정보 수정버튼시 헤더 뷰 정보 업데이트 | 2h | 2h | 27 |
xcode 15.0.1, 최소버전 iOS 15.0, swift 5.9
라이브러리 | 사용목적 |
---|---|
RxSwift, RxCocoa | 비동기 처리, MVVM 패턴 및 UI 바인딩 |
RealmSwift | DB |
Snapkit | 코드베이스UI 구현 |
FirebaseCrashlytics, AnalyticsWithoutAdidSupport | 충돌 감지 및 앱 모니터링 |
DGChart, Tabman, FSCalendar | UI 구현 |
- 아키텍처 패턴: MVVM, MVC
- UI 프레임워크: UIKit
- 라이브러리 설치: Package Manager
PHPicker로 카메라로 찍은 사진 또는 갤러리에서 가지고 온 이미지를 png() 함수를 통해 이미지 데이터를 디렉토리에 저장한 후, 저장한 이미지를 UIImage로 보여 줄 경우 다음과 같이 사진의 방향이 회전되던 문제가 발생했다. png() 대신에 jpeg()를 사용해서 jpeg 데이터 형식으로 이미지를 저장했더니 정상적으로 이미지가 나왔다.
이러한 문제가 생긴 원인은 데이터를 읽어올때 파일의 형식 따라서 지원하는 메타데이터가 다르기 때문이다.

iOS에서는 사진을 찍을때 왼쪽 사진과 같이 세로모드로 찍어도 실제로는 오른쪽 사진처럼 이미지가 저장된다. 사진이 저장될 때 메타데이터로 회전된 사진의 방향이 함께 저장된다. UIImage에서 사진을 렌더링 하기 이전에 해당 메타데이터를 읽어 비트맵을 회전시킨 후 이미지를 렌더링 한다. 이때 iOS에서 사진이 저장될때 HEIF 또는 JPEG로 사진이 저장되는데, iOS에서 이 파일 형식을 읽어오거나 저장할떄는 exif 데이터가 포함되있지만, png에서는 이를 포함하지 않는다. 기기에서 찍은 사진을 시뮬레이터에 넣고, png, jpeg 데이터로 각각 저장했을때 다음과 같이 메타데이터를 확인 할 수 있었다.

따라서 png로 저장 할 시 이미지 회전 정보가 제외되어 저장되므로, 원래 저장된 파일 그대로 화면에 나온것이고, jpeg로 저장할 시 회전 정보가 있으므로 이미지가 제대로 나오게 된 것이다.
애완동물 종 선택 화면은 종을 추가 및 삭제하거나 세부적인 종이 나타날때 보다 자연스러워 지도록 UICollectionViewDiffableDataSource를 이용하여 애니메이션이 들어가도록 구현 했다. 새로운 종을 추가하거나 삭제, 수정을할때 �apply를 이용하여 변경사항을 업데이트를 하였는데, 기존 종을 수정할 시 apply를 해도 셀의 내용이 변경이 되지 않는 이슈가 발생.
단순히 apply를 할 경우 UICollectionViewDiffableDataSource.CellProvider는 새로 추가되거나 바뀐 Item에 대해서만 호출이 된다. 따라서 기존에 존재하던 셀의 hash값이 변경되지 않았으므로 CellProvider가 호출 되지 않으므로 인해 값의 변경이 적용되지 않았던 것이다. 그래서 apply가 아닌 항목을 reload를 해야 하는데, 이때 reloadItems, reloadSections, reconfigureItems등 reload하는 방법이 여럿 존재하는데 reloadData, reloadSections는 셀 자체를 새로운 셀로 교체하는 방식이고 reconfigureItems는 기존의 셀을 그대로 재사용하는 형식이다. 나의 경우에는 셀을 재생성할 필요 없이 내부의 내용만 바꾸면 되므로 reconfigureItems를 이용하여 셀을 다시 재구성 하는 방식을 택했다.
인스타그램이나 트위터, 네이버 카페 앱등 프로필 화면에서 자주 사용되는 UI인 nestedScrollView를 두개의 ScrollView를 이용해서 구현. 상단 탭바는 TabMan 라이브러리를 사용했다.
- overlay scrollview: 스크롤의 contentOffset을 계산하는 스크롤 뷰
- container scrollview: UI가 들어갈 스크롤 뷰.
- container scrollview Content View: HeaderView와 하단 탭바뷰로 구성
KVO를 이용해 현재 선택된 탭바의 ScrollView의 contentSize의 height + height + 탭바의 높이를 계산하여 overlay scrollView의 contentSize의 height를 변경. overlayScrollView의 �contentSize를 변경된 contentSize로 만듬. 그 후 containerScrollView와 overlayScrollView의 pan Gesture를 연결 후, overlayScrollView를 delegate로 contentOffset값을 받아서 containerScrollView의 contentOffset을 계산하는 방식으로 구현.
extension ProfileViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
contentOffsets[currentIndex] = scrollView.contentOffset.y
delegate?.scroll(contentOffset: scrollView.contentOffset)
let topHeight = bottomViewController.view.frame.minY - (delegate?.minHeaderHeight() ?? 0)
if scrollView.contentOffset.y < topHeight{
self.containerScrollView.contentOffset.y = scrollView.contentOffset.y
self.panViews.forEach({ (arg0) in
let (_, value) = arg0
(value as? UIScrollView)?.contentOffset.y = 0
})
contentOffsets.removeAll()
}else{
self.containerScrollView.contentOffset.y = topHeight
(self.panViews[currentIndex] as? UIScrollView)?.contentOffset.y = scrollView.contentOffset.y - self.containerScrollView.contentOffset.y
}
}
}