Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] 여정 기록 로직 개선 #347

Merged
20 commits merged into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0cb4ae6
:sparkles: PersistentStorage에 subpath를 적용할 수 있도록 기능 추가
SwiftyJunnos Jan 9, 2024
c502c30
:recycle: LocalRecordingManager 리팩토링
SwiftyJunnos Jan 9, 2024
e160f50
:truck: Util 디렉토리명 Storage로 수정
SwiftyJunnos Jan 9, 2024
092e7c6
:sparkles: 새로운 여정 기록 시 이전 기록이 남아있다면 삭제하는 로직 추가
SwiftyJunnos Jan 9, 2024
2b824dd
:sparkles: Spot과 좌표값들이 이어서 작성되도록 수정
SwiftyJunnos Jan 10, 2024
5e5c715
:sparkles: 여정 기록 중 앱 종료 시 다음 실행 때 이어서 실행되도록 수정
SwiftyJunnos Jan 10, 2024
9d52ea1
:rocket: 0.6.2 버전업
SwiftyJunnos Jan 10, 2024
3409893
:bug: Spot 완료 Navigation 버그 수정
SwiftyJunnos Jan 10, 2024
0390082
:sparkles: 여기서 다시 검색 버튼이 여정 기록 중에 보여지던 문제 수정
SwiftyJunnos Jan 10, 2024
787b2cb
:recycle: 여정 기록 컨트롤 파편화 개선
SwiftyJunnos Jan 10, 2024
20c1280
:rocket: 빌드 버전 2로 업
SwiftyJunnos Jan 10, 2024
bae2e0d
:test_tube: RecordingJourneyStorage 변경에 따른 유닛테스트 수정
SwiftyJunnos Jan 10, 2024
5a0c3a4
:bug: Spot 화면이 close 되지 않는 현상 수정
SwiftyJunnos Jan 10, 2024
26ccb3b
:bug: Spot 카메라 사용 시 오류 수정
SwiftyJunnos Jan 10, 2024
a0b8013
:rocket: 0.6.2(3) 버전 업
SwiftyJunnos Jan 10, 2024
8f04848
:bug: 여정 취소 네트워킹이 실패한 경우에도 동작할 수 있도록 수정
SwiftyJunnos Jan 10, 2024
b34a101
:bug: 여기서 다시 검색 시 이전 어노테이션이 삭제되도록 수정
SwiftyJunnos Jan 10, 2024
51306d1
:bug: 여정 기록 중 스팟 후 어노테이션이 지도 상에 추가되도록 수정
SwiftyJunnos Jan 11, 2024
0a574d0
:sparkles: 여정 저장 시 버튼에 로딩 인디케이터 추가
SwiftyJunnos Jan 11, 2024
db2c736
:bug: 여정 완료 후에도 여정이 기록되는 현상 수정
SwiftyJunnos Jan 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ import MSDomain
public protocol HomeNavigationDelegate: AnyObject {

func navigateToSpot(spotCoordinate coordinate: Coordinate)
func navigateToSelectSong(lastCoordinate: Coordinate)
func navigateToSaveJourneyFlow(lastCoordinate: Coordinate)

}
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,35 @@ public final class HomeViewController: HomeBottomSheetViewController {
// MARK: - Combine Binding

private func bind() {
self.viewModel.state.startedJourney
self.viewModel.state.journeyDidStarted
.receive(on: DispatchQueue.main)
.sink { [weak self] startedJourney in
self?.contentViewController.recordingShouldStart(startedJourney)
self?.contentViewController.clearOverlays()
self?.contentViewController.recordingDidStart(startedJourney)
}
.store(in: &self.cancellables)

self.viewModel.state.journeyDidResumed
.receive(on: DispatchQueue.main)
.sink { [weak self] resumedJourney in
self?.contentViewController.clearOverlays()
self?.contentViewController.recordingDidResume(resumedJourney)
}
.store(in: &self.cancellables)

self.viewModel.state.journeyDidCancelled
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
if case .failure = completion {
self?.contentViewController.clearOverlays()
self?.contentViewController.recordingDidStop()
}
}, receiveValue: { [weak self] cancelledJourney in
self?.contentViewController.clearOverlays()
self?.contentViewController.recordingDidStop(cancelledJourney)
})
.store(in: &self.cancellables)

self.viewModel.state.visibleJourneys
.sink { [weak self] visibleJourneys in
self?.contentViewController.visibleJourneysDidUpdated(visibleJourneys)
Expand All @@ -155,7 +177,6 @@ public final class HomeViewController: HomeBottomSheetViewController {
self?.hideBottomSheet()
} else {
self?.showBottomSheet()
self?.contentViewController.recordingShouldStop(isCancelling: false)
}
self?.updateButtonMode(isRecording: isRecording)
}
Expand All @@ -169,37 +190,39 @@ public final class HomeViewController: HomeBottomSheetViewController {
.store(in: &self.cancellables)

self.viewModel.state.isRefreshButtonHidden
.removeDuplicates(by: { $0 == $1 })
.receive(on: DispatchQueue.main)
.sink { [weak self] isHidden in
self?.refreshButton.isHidden = isHidden
}
.store(in: &self.cancellables)

self.viewModel.state.overlaysShouldBeCleared
.combineLatest(self.viewModel.state.isRecording)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.contentViewController.clearOverlays()
self?.contentViewController.clearAnnotations()
.sink { [weak self] isHidden, isRecording in
guard let self = self else { return }
UIView.transition(with: self.refreshButton,
duration: 0.2,
options: .transitionCrossDissolve) { [weak self] in
self?.refreshButton.isHidden = (isHidden || isRecording)
}
}
.store(in: &self.cancellables)
}

// MARK: - Functions

private func updateButtonMode(isRecording: Bool) {
UIView.transition(with: startButton, duration: 0.5,
UIView.transition(with: self.view,
duration: 0.34,
options: .transitionCrossDissolve,
animations: { [weak self] in
self?.startButton.isHidden = isRecording
})
UIView.transition(with: recordJourneyButtonStackView, duration: 0.5,
options: .transitionCrossDissolve,
animations: { [weak self] in
self?.recordJourneyButtonStackView.isHidden = !isRecording
})
}

public func spotDidAdded(_ spot: Spot, photoData: Data) {
self.contentViewController.spotDidAdded(spot, photoData: photoData)
}

public func journeyDidEnded(endedJourney: Journey) {
self.contentViewController.recordingDidStop(RecordingJourney(endedJourney))
}

}

// MARK: - Buttons
Expand Down Expand Up @@ -231,14 +254,13 @@ extension HomeViewController: RecordJourneyButtonViewDelegate {
self.viewModel.trigger(.refreshButtonDidTap(visibleCoordinates: coordinates))
}

public func backButtonDidTap(_ button: MSRectButton) {
func backButtonDidTap(_ button: MSRectButton) {
guard self.viewModel.state.isRecording.value == true else { return }

self.viewModel.trigger(.backButtonDidTap)
self.contentViewController.recordingShouldStop(isCancelling: true)
}

public func spotButtonDidTap(_ button: MSRectButton) {
func spotButtonDidTap(_ button: MSRectButton) {
guard self.viewModel.state.isRecording.value == true else { return }

guard let currentUserCoordiante = self.contentViewController.currentUserCoordinate else {
Expand All @@ -248,14 +270,14 @@ extension HomeViewController: RecordJourneyButtonViewDelegate {
self.navigationDelegate?.navigateToSpot(spotCoordinate: currentUserCoordiante)
}

public func nextButtonDidTap(_ button: MSRectButton) {
func nextButtonDidTap(_ button: MSRectButton) {
guard self.viewModel.state.isRecording.value == true else { return }

guard let currentUserCoordiante = self.contentViewController.currentUserCoordinate else {
return
}

self.navigationDelegate?.navigateToSelectSong(lastCoordinate: currentUserCoordiante)
self.navigationDelegate?.navigateToSaveJourneyFlow(lastCoordinate: currentUserCoordiante)
}

}
Expand Down
82 changes: 64 additions & 18 deletions iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ public final class HomeViewModel {
case startButtonDidTap(Coordinate)
case refreshButtonDidTap(visibleCoordinates: (minCoordinate: Coordinate, maxCoordinate: Coordinate))
case backButtonDidTap
case recordingStateDidChange(Bool)
case mapViewDidChange
}

public struct State {
// Passthrough
public var startedJourney = PassthroughSubject<RecordingJourney, Never>()
public var journeyDidStarted = PassthroughSubject<RecordingJourney, Never>()
public var journeyDidResumed = PassthroughSubject<RecordingJourney, Never>()
public var journeyDidCancelled = PassthroughSubject<RecordingJourney, Error>()
public var visibleJourneys = PassthroughSubject<[Journey], Never>()
public var overlaysShouldBeCleared = PassthroughSubject<Bool, Never>()

// CurrentValue
public var isRecording = CurrentValueSubject<Bool, Never>(false)
Expand Down Expand Up @@ -68,22 +70,25 @@ public final class HomeViewModel {
#endif

self.createNewUserWhenFirstLaunch()

self.resumeJourneyIfNeeded()
case .viewNeedsReloaded:
let isRecording = self.journeyRepository.fetchIsRecording()
self.state.isRecording.send(isRecording)
self.syncRecordingState()
case .startButtonDidTap(let coordinate):
#if DEBUG
MSLogger.make(category: .home).debug("Start 버튼 탭: \(coordinate)")
MSLogger.make(category: .home).debug("시작 버튼이 탭 되었습니다: \(coordinate)")
#endif
self.startJourney(at: coordinate)
self.state.isRefreshButtonHidden.send(true)
case .refreshButtonDidTap(visibleCoordinates: (let minCoordinate, let maxCoordinate)):
self.state.isRefreshButtonHidden.send(true)
self.fetchJourneys(minCoordinate: minCoordinate, maxCoordinate: maxCoordinate)
case .backButtonDidTap:
self.state.isRecording.send(false)
self.state.isRefreshButtonHidden.send(false)
self.state.overlaysShouldBeCleared.send(true)
#if DEBUG
MSLogger.make(category: .home).debug("취소 버튼이 탭 되었습니다.")
#endif
self.cancelJourney()
case .recordingStateDidChange(let isRecording):
self.state.isRecording.send(isRecording)
case .mapViewDidChange:
if self.state.isRecording.value == false {
self.state.isRefreshButtonHidden.send(false)
Expand Down Expand Up @@ -118,8 +123,26 @@ private extension HomeViewModel {
}
}

func fetchJourneys(minCoordinate: Coordinate, maxCoordinate: Coordinate) {
guard let userID = self.userRepository.fetchUUID() else { return }

Task {
let result = await self.journeyRepository.fetchJourneyList(userID: userID,
minCoordinate: minCoordinate,
maxCoordinate: maxCoordinate)
switch result {
case .success(let journeys):
self.state.visibleJourneys.send(journeys)
case .failure(let error):
MSLogger.make(category: .home).error("\(error)")
}
}
}

func startJourney(at coordinate: Coordinate) {
Task {
defer { self.syncRecordingState() }

self.state.isStartButtonLoading.send(true)
defer { self.state.isStartButtonLoading.send(false) }

Expand All @@ -128,28 +151,51 @@ private extension HomeViewModel {
let result = await self.journeyRepository.startJourney(at: coordinate, userID: userID)
switch result {
case .success(let recordingJourney):
self.state.startedJourney.send(recordingJourney)
self.state.isRecording.send(true)
self.state.journeyDidStarted.send(recordingJourney)
self.state.isRefreshButtonHidden.send(true)
case .failure(let error):
MSLogger.make(category: .home).error("\(error)")
}
}
}

func fetchJourneys(minCoordinate: Coordinate, maxCoordinate: Coordinate) {
guard let userID = self.userRepository.fetchUUID() else { return }
func resumeJourneyIfNeeded() {
defer { self.syncRecordingState() }

guard let recordingJourney = self.journeyRepository.fetchRecordingJourney() else {
return
}

self.state.journeyDidResumed.send(recordingJourney)
}

func cancelJourney() {
guard let userID = self.userRepository.fetchUUID(),
let recordingJourney = self.journeyRepository.fetchRecordingJourney() else {
return
}

Task {
let result = await self.journeyRepository.fetchJourneyList(userID: userID,
minCoordinate: minCoordinate,
maxCoordinate: maxCoordinate)
defer { self.syncRecordingState() }

let result = await self.journeyRepository.deleteJourney(recordingJourney, userID: userID)
switch result {
case .success(let journeys):
self.state.visibleJourneys.send(journeys)
case .success(let deletedJourney):
self.state.journeyDidCancelled.send(deletedJourney)
case .failure(let error):
MSLogger.make(category: .home).error("\(error)")
self.state.journeyDidCancelled.send(completion: .failure(error))
}
}
}

func syncRecordingState() {
let isRecording = self.journeyRepository.isRecording
#if DEBUG
MSLogger.make(category: .home)
.debug("여정 기록 여부를 싱크하고 있습니다. 현재 기록 상태: \(isRecording ? "기록 중" : "기록 중이지 않음")")
#endif
self.state.isRecording.send(isRecording)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import UIKit
import MSDesignSystem
import MSUIKit

public protocol RecordJourneyButtonViewDelegate: AnyObject {
internal protocol RecordJourneyButtonViewDelegate: AnyObject {

func backButtonDidTap(_ button: MSRectButton)
func spotButtonDidTap(_ button: MSRectButton)
Expand Down Expand Up @@ -64,7 +64,7 @@ public final class RecordJourneyButtonStackView: UIView {

// MARK: - Properties

public var delegate: RecordJourneyButtonViewDelegate?
internal var delegate: RecordJourneyButtonViewDelegate?

// MARK: - Initializer

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by 이창준 on 2023.12.10.
//

import CoreLocation
import Foundation

import MSData
Expand All @@ -27,7 +28,7 @@ extension MapViewController {

extension MapViewController {

public func recordingShouldStart(_ startedJourney: RecordingJourney) {
public func recordingDidStart(_ startedJourney: RecordingJourney) {
guard self.viewModel is NavigateMapViewModel else {
MSLogger.make(category: .home).error("여정이 시작되어야 하지만 이미 Map에서 RecordJourneyViewModel을 사용하고 있습니다.")
return
Expand All @@ -44,18 +45,36 @@ extension MapViewController {
self.locationManager.allowsBackgroundLocationUpdates = true

#if DEBUG
MSLogger.make(category: .home).debug("여정 기록이 시작되었습니다.")
MSLogger.make(category: .home).debug("여정 기록이 시작되었습니다: \(startedJourney)")
#endif
}

public func recordingShouldStop(isCancelling: Bool) {
guard let viewModel = self.viewModel as? RecordJourneyViewModel else {
MSLogger.make(category: .home).error("여정이 종료되어야 하지만 이미 Map에서 NavigateMapViewModel을 사용하고 있습니다.")
return
public func recordingDidResume(_ recordedJourney: RecordingJourney) {
let userRepository = UserRepositoryImplementation()
let journeyRepository = JourneyRepositoryImplementation()
let recordJourneyViewModel = RecordJourneyViewModel(startedJourney: recordedJourney,
userRepository: userRepository,
journeyRepository: journeyRepository)
self.swapViewModel(to: recordJourneyViewModel)

let coordinates = recordedJourney.coordinates.map {
CLLocationCoordinate2D(latitude: $0.latitude,
longitude: $0.longitude)
}
self.drawPolyline(using: coordinates)

self.locationManager.startUpdatingLocation()
self.locationManager.allowsBackgroundLocationUpdates = true

if isCancelling {
viewModel.trigger(.recordingDidCancelled)
#if DEBUG
MSLogger.make(category: .home).debug("여정 기록이 재개되었습니다: \(recordedJourney)")
#endif
}

public func recordingDidStop(_ stoppedJourney: RecordingJourney? = nil) {
guard self.viewModel is RecordJourneyViewModel else {
MSLogger.make(category: .home).error("여정이 종료되어야 하지만 이미 Map에서 NavigateMapViewModel을 사용하고 있습니다.")
return
}

let journeyRepository = JourneyRepositoryImplementation()
Expand All @@ -66,8 +85,25 @@ extension MapViewController {
self.locationManager.allowsBackgroundLocationUpdates = false

#if DEBUG
MSLogger.make(category: .home).debug("여정 기록이 종료되었습니다.")
if let stoppedJourney {
MSLogger.make(category: .home).debug("여정 기록이 종료되었습니다: \(stoppedJourney)")
}
#endif
}

}

// MARK: - Spot

extension MapViewController {

public func spotDidAdded(_ spot: Spot, photoData: Data) {
let coordinate = CLLocationCoordinate2D(latitude: spot.coordinate.latitude,
longitude: spot.coordinate.longitude)

self.addAnnotation(title: spot.timestamp.description,
coordinate: coordinate,
photoData: photoData)
}

}
Loading