- 인원 : 개인프로젝트
- 기간 : 2023.09.24 ~ 2023.10.22 (4주)
- 최소지원버전 : iOS 16
- 국내 관광지에 대한 정보를 알수 있고 관광지 가이드 오디오를 들을 수 있는 앱 입니다.
Category | Stack |
---|---|
Architecture | MVVM |
iOS | UIKit AVFoundation WebKit MapKit UserDefaults CoreLocation |
UI | SnapKit |
Reactive | RxSwift |
Network | URLSession Codable Alamofire |
Database | Realm |
Image | Kingfisher |
Dependency Manager | SPM CocoaPods |
Firebase | Crashlytics Analytics |
Etc | Hero Parchment FSPagerView IQKeyboardManager FloatingPanel Charts Toast |
- 관광지 오디오 가이드 재생 (백그라운드,잠금화면 오디오 재생)
- 오디오 가이드 좋아요
- 관광지 / 가이드 검색
- 오디오 가이드 파일 공유
- 위치 기반 관광지 오디오 검색
- 관광지 방문객 통계
- CollectionView - DiffableDataSource / CompositionalLayout 적용
- AVPlayer, MPRemoteCommandCenter 클래스 더이상 사용되지 않을 때 메모리에서 해지 되도록 구현
- MPRemoteCommandCenter 클래스를 활용해 잠금화면과 다이나믹 아일랜드에서 재생/일시정지 기능 구현
- AVPlayer, MPNowPlayingInfoCenter 의 재생 상태 동기화
- MapKit CustomAnnotationView / CustomClustering 구현
- Kingfisher, 서버 이미지 재가공후 Annotation 이미지로 사용
- Alamofire Router 패턴으로 Request 관리 / RequestInterceptor 프로토콜을 활용해 동일한 인자값들 defaultParameters 로 관리
- 비동기 함수 async / await 적용 후 가독성 개선
- URLScheme 외부 지도앱 연동
- 다크모드, 다국어 대응
✏️ 트러블슈팅 일지
-> 오디오를 재생시킬 때 MPRemoteCommandCenter 의 재생/일시정지 기능이 동작하도록 MPRemoteCommandCenter 를 등록하는 과정이 필요했습니다.
등록할 때 계속 addTarget 이 호출되어 이전에 등록된 action 이 해지되지 않아 중첩되고 있었습니다. 해당 메서드에 removeTarget 을 추가 후 해결했습니다.
func registerRemoteCenterAction() {
// 중첨 액션 방지용
center.playCommand.removeTarget(self)
center.pauseCommand.removeTarget(self)
center.playCommand.isEnabled = true
center.pauseCommand.isEnabled = true
let player = AVPlayerManager.shared.player
center.playCommand.addTarget { event in
AVPlayerManager.shared.replay()
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player?.currentTime().seconds
return .success
}
center.pauseCommand.addTarget { event in
AVPlayerManager.shared.pause()
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player?.currentTime().seconds
return .success
}
}
-> 서버 이미지가 있는 경우에도 Default 이미지가 보이는 오류가 있었습니다.
이미지 용량이 작은 것은 로드가 빨리 되고 큰 것은 로드가 느려서 나타나는 문제로 파악했습니다.
-> MKMapViewDelegate 프로토콜의 viewFor 메서드에서 이미지 URL 전달 후 MKAnnotationView 의 annotation 이 View 에 표시되기 전에 호출 되는 prepareForDisplay() 에서 이미지를 load 하도록 했습니다.
extension DetailRegionMapVC: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !annotation.isKind(of: MKUserLocation.self) else { return nil }
switch annotation {
case is MKClusterAnnotation:
let clusterAnotaionView = mapView.dequeueReusableAnnotationView(
withIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier, for: annotation) as? ClusterAnnotationView
return clusterAnotaionView
case is StoryAnnotation:
let customAnnotation = annotation as? StoryAnnotation
guard let customAnnotation else { return nil }
let annotationView = mapView.dequeueReusableAnnotationView(
withIdentifier: CustomAnnotationView.identifier, for: customAnnotation) as? CustomAnnotationView
guard let annotationView else { return nil }
let customAnnotaionImageUrl = stories.first {
$0 == customAnnotation.item
}?.imageURL ?? ""
annotationView.createImage(imagePath: customAnnotaionImageUrl)
return annotationView
default:
return nil
}
}
}
final class CustomAnnotationView: MKAnnotationView {
private var thumbnail: UIImage?
...
override func prepareForDisplay() {
super.prepareForDisplay()
annotationImageView.image = nil
annotationImageView.image = thumbnail == nil ? .defaultImg : thumbnail
}
...
func createImage(imagePath: String) {
if imagePath.isEmpty {
thumbnail = .defaultImg
} else {
downloadImage(with: imagePath) { image in
guard let image else {
self.thumbnail = .defaultImg
return
}
self.thumbnail = image
}
}
annotationImageView.image = thumbnail
}
}
-> View 구조는 VisualEffectView(BlurEffect) > View > VisualEffectView(VibrancyEffect) > View > UIButton 구조였습니다.
Button 위에 여러 개의 View 가 쌓이면서 Button 의 터치이벤트가 상위 뷰에게 터치 액션을 빼앗겼습니다.
-> 하위뷰로 터치 이벤트를 넘기기 위해 hitTest 를 활용해 해결했습니다.
extension UIVisualEffectView {
override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self.contentView ? nil : view
}
}
✏️ 회고 일지
- Swift 언어에 대한 이해도는 앱이 커질 수록 점점 중요해지는 부분이라고 느꼈습니다.
WMO, Struct, Reference Count, final 이런 키워드들에 대한 내용을 잘 이해하고 있어야 Swift 성능을 향상시킬 수 있기 때문에 앞으로도 꾸준히 학습이 필요하다고 느꼈습니다. - 개발자는 생각의 폭을 넓게 가질 수 있어야 합니다. 이런 부분까지 신경써야되냐? 라고 한다면 어떤 사용자가 어떤 환경에서 사용할지 모르기 때문에 신경써야 한다고 생각합니다.
기능 개발을 빨리 할 수 있는 실력이 키워진다면 여러 상황에서 어떻게 동작이 되어질까? 에 대한 부분을 미리 알고 미리 대응할 수 있어야한다고 생각합니다.
출시 버전에서는 이러한 부분을 많이 챙기지 못했지만 이후 업데이트 버전은 다양한 상황들에 대해 좀더 고민해보고 그에 맞게 업데이트를 진행할 예정입니다.
이터레이션 | 기간 | 타입 |
---|---|---|
이터레이션 0 | 09.24 | 프로젝트 세팅 |
이터레이션 1 | 09.25 ~ 09.27 | 개발 |
이터레이션 2 | 09.28 ~ 10.01 | 개발 |
이터레이션 3 | 10.02 ~ 10.04 | 개발 |
이터레이션 4 | 10.05 ~ 10.08 | 개발 |
이터레이션 5 | 10.09 ~ 10.11 | 개발 |
이터레이션 6 | 10.12 ~ 10.15 | 개발 |
이터레이션 7 | 10.16 ~ 10.18 | QA/출시준비 |
이터레이션 8 | 10.19 ~ 10.22 | QA/출시준비 |
이터레이션 9 | 10.23 ~ 10.25 | 심사 기간 |
Task | 이터레이션 | 예상 (hour) | 구현 (hour) | 완료 | 비고 |
---|---|---|---|---|---|
프로젝트 세팅 | 0 | 6 | 12 | 다국어, 다크모드, 라이브러리, Base 등 세팅 | |
홈 메인 UI | 1 | 8 | 12 | Cocoapods 오류 처리로 지연 | |
Splash UI & 기능 | 1 | 8 | 13 | 기획단계에서 생각만 했던 것을 개발하며 기획을 같이해서 지연 | |
테마 상세 UI | 1 | 8 | 16 | hero 라이브러리 기능 개발 지연, UIVisualEffectView 안 객체 터치 안되는 이슈로 지연 HitTest 로 해결 | |
테마 상세 기능 | 2 | 10 | 20 | 테마상세로 이동시 api 최초 한번만 호출 데이터 캐시 기능 구현으로 지연, 좋아요, 재생 관련기능이 생각보다 처리할게 많아 지연 | |
테마 상세 지도모드 UI | 2 | 4 | - h | 보류 | |
(커스텀뷰)하단 재생 UI | 2 | 2 | 3 | 없음 | |
(커스텀뷰)하단 재생 기능 | 2 | 8 | 6 | 없음 | |
검색 메인 UI | 2 | 6 | 22 | DiffableDataSource, CompositionalLayout 학습 지연 | |
검색 메인 기능 | 3 | 10 | 6 | 없음 | |
검색 결과 UI | 3 | 6 | 12 | tabMan -> parchment 라이브러리 변경 | |
검색 결과 기능 | 3 | 8 | 12 | 부모ViewController 와 자식ViewController 간의 데이터 전달 지연 | |
관광지 상세 UI | 3 | 4 | 20 | 개인 일정으로 개발 진행 못함 | |
관광지 상세 기능 UI | 3 | 6 | 6 | 없음 | |
이야기 상세 UI | 4 | 4 | 6 | 없음 | |
이야기 상세 기능 | 4 | 6 | 4 | 없음 | |
지도 메인 UI | 5 | 3 | 5 | FloatingPanel 라이브러리 학습 | |
지도 메인 줌인 기능 | 5 | 2 | 2 | 없음 | |
지도 관광지 상세 UI | 5 | 8 | 8 | 없음 | |
지도 관광지 상세 기능 | 5 | 8 | 6 | 없음 | |
지도 상세 커스텀 마커 | 5 | 2 | 3 | 없음 | |
지도 상세 클러스터링 | 6 | 5 | 3 | 없음 | |
지도 상세 UI | 6 | 6 | 6 | 없음 | |
지도 상세 마커, 클러스터링 터치 기능 | 6 | 10 | 8 | 없음 | |
설정 메인 UI | 7 | 8 | 5 | 없음 | |
설정 메인 기능 | 7 | 16 | 12 | 없음 | |
로컬 알림 기능 | 7 | 3 | 6 | 로컬 알림을 등록하는 부분보다 등록 후 알림설정 페이지에서 권한이 변경되었을 때 처리하는 로직에 시간이 많이 듬 | |
잠금모드일 때 오디오 재생 기능 | 7 | 6 | 8 | MPRemoteCommandCenter 를 이용해 테스트 코드 작성 후 실제 적용시키는데 지연됨 |