From 0cb4ae64dc1c38ec9bd078d13522650029fe9dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Tue, 9 Jan 2024 16:28:36 +0900 Subject: [PATCH 01/20] =?UTF-8?q?:sparkles:=20PersistentStorage=EC=97=90?= =?UTF-8?q?=20subpath=EB=A5=BC=20=EC=A0=81=EC=9A=A9=ED=95=A0=20=EC=88=98?= =?UTF-8?q?=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FileManager/FileManagerStorage.swift | 41 +++++++++++++------ .../Protocol/MSPersistentStorage.swift | 22 +++++++++- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/iOS/MSCoreKit/Sources/MSPersistentStorage/FileManager/FileManagerStorage.swift b/iOS/MSCoreKit/Sources/MSPersistentStorage/FileManager/FileManagerStorage.swift index 4c54136..7cf5c49 100644 --- a/iOS/MSCoreKit/Sources/MSPersistentStorage/FileManager/FileManagerStorage.swift +++ b/iOS/MSCoreKit/Sources/MSPersistentStorage/FileManager/FileManagerStorage.swift @@ -59,8 +59,10 @@ public final class FileManagerStorage: NSObject, MSPersistentStorage { /// - type: 불러올 값의 타입 /// - key: 불러올 값에 대응되는 Key 값 /// - Returns: `FileManager`에서 불러온 뒤 지정된 타입으로 디코딩 된 값 - public func get(_ type: T.Type, forKey key: String) -> T? { - guard let fileURL = self.fileURL(forKey: key), + public func get(_ type: T.Type, + forKey key: String, + subpath: String? = nil) -> T? { + guard let fileURL = self.fileURL(forKey: key, subpath: subpath), let data = try? Data(contentsOf: fileURL) else { return nil } @@ -104,8 +106,10 @@ public final class FileManagerStorage: NSObject, MSPersistentStorage { /// `FileManager`를 사용한 저장에 성공했을 경우 요청한 데이터를 반환합니다. \ /// 저장에 실패했거나 이미 존재한다면 `nil`을 반환합니다. @discardableResult - public func set(value: T, forKey key: String) -> T? { - guard let fileURL = self.fileURL(forKey: key) else { + public func set(value: T, + forKey key: String, + subpath: String? = nil) -> T? { + guard let fileURL = self.fileURL(forKey: key, subpath: subpath) else { return nil } @@ -124,6 +128,12 @@ public final class FileManagerStorage: NSObject, MSPersistentStorage { } } + public func delete(forKey key: String) throws { + if let path = self.fileURL(forKey: key) { + try self.fileManager.removeItem(at: path) + } + } + public func deleteAll() throws { if let path = self.storageURL()?.path, self.fileManager.fileExists(atPath: path) { @@ -143,8 +153,8 @@ extension FileManagerStorage { /// - Returns: /// Storage가 사용하는 디렉토리 URL. \ /// 휙득에 실패했거나, `create` flag가 `true`일 때 생성에 실패했을 경우 `nil`을 반환합니다. - func storageURL(create: Bool = false) -> URL? { - let directoryURL: URL? + func storageURL(subpath: String? = nil, create: Bool = false) -> URL? { + var directoryURL: URL? if #available(iOS 16.0, *) { let storageDirectoryURL = try? self.fileManager.url(for: .cachesDirectory, in: .userDomainMask, @@ -152,12 +162,18 @@ extension FileManagerStorage { create: false) directoryURL = storageDirectoryURL? .appending(path: Constants.appBundleIdentifier, directoryHint: .isDirectory) + if let subpath = subpath { + directoryURL = directoryURL?.appending(path: subpath, directoryHint: .isDirectory) + } } else { let cacheDirectoryURL = self.fileManager .urls(for: .cachesDirectory, in: .userDomainMask) .first directoryURL = cacheDirectoryURL? .appendingPathComponent(Constants.appBundleIdentifier, isDirectory: true) + if let subpath = subpath { + directoryURL = directoryURL?.appendingPathComponent(subpath, isDirectory: true) + } } if create { @@ -181,18 +197,19 @@ extension FileManagerStorage { /// - Returns: /// 지정된 Key 값에 대응되는 파일의 URL. \ /// 휙득에 실패했을 경우 `nil`을 반환합니다. - func fileURL(forKey key: String) -> URL? { + func fileURL(forKey key: String, subpath: String? = nil) -> URL? { + let storageURL = self.storageURL(subpath: subpath, create: true) + let fileURL: URL? + let fileExtension = "json" if #available(iOS 16.0, *) { - fileURL = self.storageURL(create: true)? + fileURL = storageURL? .appending(component: key, directoryHint: .notDirectory) - .appendingPathExtension("json") } else { - fileURL = self.storageURL(create: true)? + fileURL = storageURL? .appendingPathComponent(key, isDirectory: false) - .appendingPathExtension("json") } - return fileURL + return fileURL?.appendingPathExtension(fileExtension) } /// 주어진 URL에 디렉토리를 생성합니다. diff --git a/iOS/MSCoreKit/Sources/MSPersistentStorage/Protocol/MSPersistentStorage.swift b/iOS/MSCoreKit/Sources/MSPersistentStorage/Protocol/MSPersistentStorage.swift index 4d1a8ea..3c22578 100644 --- a/iOS/MSCoreKit/Sources/MSPersistentStorage/Protocol/MSPersistentStorage.swift +++ b/iOS/MSCoreKit/Sources/MSPersistentStorage/Protocol/MSPersistentStorage.swift @@ -7,8 +7,26 @@ public protocol MSPersistentStorage { - func get(_ type: T.Type, forKey key: String) -> T? + func get(_ type: T.Type, forKey key: String, subpath: String?) -> T? @discardableResult - func set(value: T, forKey key: String) -> T? + func set(value: T, forKey key: String, subpath: String?) -> T? + func delete(forKey key: String, subpath: String?) throws } + +extension MSPersistentStorage { + + public func get(_ type: T.Type, forKey key: String, subpath: String? = nil) -> T? { + return self.get(type, forKey: key, subpath: nil) + } + + @discardableResult + public func set(value: T, forKey key: String, subpath: String? = nil) -> T? { + return self.set(value: value, forKey: key, subpath: subpath) + } + + public func delete(forKey key: String, subpath: String? = nil) throws { + try self.delete(forKey: key, subpath: subpath) + } + +} From c502c309788232f5e907c8c60848624a04031a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Wed, 10 Jan 2024 00:50:23 +0900 Subject: [PATCH 02/20] =?UTF-8?q?:recycle:=20LocalRecordingManager=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit static 변수를 사용하는 방식에서 FileManager를 사용하는 방식으로 리팩토링 --- .../Home/Presentation/HomeViewModel.swift | 16 +- .../Presentation/SaveJourneyViewModel.swift | 8 +- .../Spot/Presentation/SaveSpotViewModel.swift | 2 +- .../FileManager/FileManagerStorage.swift | 4 +- .../Protocol/MSPersistentStorage.swift | 5 + .../JourneyRepository.swift | 105 ++++++------ .../SpotRepository.swift | 2 +- .../MSData/Util/LocalRecordingManager.swift | 106 ------------ .../MSData/Util/RecordingJourneyStorage.swift | 158 ++++++++++++++++++ .../PersistentManagerTests.swift | 14 +- .../MSDomain/Model/RecordingJourney.swift | 22 +++ .../Repository/JourneyRepository.swift | 10 +- .../Sources/MSConstants/Constants.swift | 2 +- .../Sources/MSLogger/MSLogCategory.swift | 2 +- iOS/MusicSpot/MusicSpot/SceneDelegate.swift | 43 ----- 15 files changed, 271 insertions(+), 228 deletions(-) delete mode 100644 iOS/MSData/Sources/MSData/Util/LocalRecordingManager.swift create mode 100644 iOS/MSData/Sources/MSData/Util/RecordingJourneyStorage.swift diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift index 15bafa3..0912b8d 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift @@ -69,7 +69,13 @@ public final class HomeViewModel { self.createNewUserWhenFirstLaunch() case .viewNeedsReloaded: - let isRecording = self.journeyRepository.fetchIsRecording() + let isRecording = self.journeyRepository.isRecording + #if DEBUG + MSLogger.make(category: .home).debug("여정 기록 중 여부: \(isRecording)") + #endif + if isRecording { + self.resumeJourney() + } self.state.isRecording.send(isRecording) case .startButtonDidTap(let coordinate): #if DEBUG @@ -152,4 +158,12 @@ private extension HomeViewModel { } } + func resumeJourney() { + guard let recordingJourney = self.journeyRepository.fetchRecordingJourney() else { + return + } + + MSLogger.make(category: .home).debug("Recording Journey: \(recordingJourney)") + } + } diff --git a/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewModel.swift b/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewModel.swift index 53d7d38..ccb308f 100644 --- a/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewModel.swift +++ b/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewModel.swift @@ -75,7 +75,7 @@ public final class SaveJourneyViewModel { self.state.buttonStateFactors.send(stateFactors) } - guard let recordingJourney = self.journeyRepository.loadJourneyFromLocal() else { return } + guard let recordingJourney = self.journeyRepository.fetchRecordingJourney() else { return } self.state.recordingJourney.send(recordingJourney) case .musicControlButtonDidTap: let stateFactors = self.state.buttonStateFactors.value @@ -93,8 +93,10 @@ public final class SaveJourneyViewModel { private extension SaveJourneyViewModel { func endJourney(named title: String) { - guard let recordingJourney = self.state.recordingJourney.value else { return } - guard let journeyID = self.journeyRepository.fetchRecordingJourneyID() else { return } + guard let recordingJourney = self.state.recordingJourney.value, + let journeyID = self.journeyRepository.recordingJourneyID else { + return + } let selectedSong = self.state.selectedSong.value let coordinates = recordingJourney.coordinates + [self.lastCoordiante] diff --git a/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewModel.swift b/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewModel.swift index d5fce03..941d19b 100644 --- a/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewModel.swift +++ b/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewModel.swift @@ -53,7 +53,7 @@ internal extension SaveSpotViewModel { switch action { case .startUploadSpot(let data): Task { - guard let recordingJourneyID = self.journeyRepository.fetchRecordingJourneyID() else { + guard let recordingJourneyID = self.journeyRepository.recordingJourneyID else { MSLogger.make(category: .spot).error("recoding 중인 journeyID를 찾지 못하였습니다.") return } diff --git a/iOS/MSCoreKit/Sources/MSPersistentStorage/FileManager/FileManagerStorage.swift b/iOS/MSCoreKit/Sources/MSPersistentStorage/FileManager/FileManagerStorage.swift index 7cf5c49..5496476 100644 --- a/iOS/MSCoreKit/Sources/MSPersistentStorage/FileManager/FileManagerStorage.swift +++ b/iOS/MSCoreKit/Sources/MSPersistentStorage/FileManager/FileManagerStorage.swift @@ -134,8 +134,8 @@ public final class FileManagerStorage: NSObject, MSPersistentStorage { } } - public func deleteAll() throws { - if let path = self.storageURL()?.path, + public func deleteAll(subpath: String? = nil) throws { + if let path = self.storageURL(subpath: subpath)?.path, self.fileManager.fileExists(atPath: path) { try self.fileManager.removeItem(atPath: path) } diff --git a/iOS/MSCoreKit/Sources/MSPersistentStorage/Protocol/MSPersistentStorage.swift b/iOS/MSCoreKit/Sources/MSPersistentStorage/Protocol/MSPersistentStorage.swift index 3c22578..305ec17 100644 --- a/iOS/MSCoreKit/Sources/MSPersistentStorage/Protocol/MSPersistentStorage.swift +++ b/iOS/MSCoreKit/Sources/MSPersistentStorage/Protocol/MSPersistentStorage.swift @@ -11,6 +11,7 @@ public protocol MSPersistentStorage { @discardableResult func set(value: T, forKey key: String, subpath: String?) -> T? func delete(forKey key: String, subpath: String?) throws + func deleteAll(subpath: String?) throws } @@ -29,4 +30,8 @@ extension MSPersistentStorage { try self.delete(forKey: key, subpath: subpath) } + public func deleteAll(subpath: String? = nil) throws { + try self.deleteAll(subpath: subpath) + } + } diff --git a/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift b/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift index bdab757..259d1b3 100644 --- a/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift +++ b/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift @@ -8,25 +8,27 @@ import Combine import Foundation -import MSConstants import MSDomain import MSLogger import MSNetworking import MSPersistentStorage -import MSUserDefaults public struct JourneyRepositoryImplementation: JourneyRepository { // MARK: - Properties private let networking: MSNetworking - public let storage: MSPersistentStorage + private let storage: MSPersistentStorage - @UserDefaultsWrapped(UserDefaultsKey.isRecording, defaultValue: false) - private var isRecording: Bool + private var recordingJourney: RecordingJourneyStorage - @UserDefaultsWrapped(UserDefaultsKey.recordingJourneyID, defaultValue: nil) - private var recordingJourneyID: String? + public var isRecording: Bool { + return self.recordingJourney.isRecording + } + + public var recordingJourneyID: String? { + return self.recordingJourney.id + } // MARK: - Initializer @@ -34,32 +36,11 @@ public struct JourneyRepositoryImplementation: JourneyRepository { persistentStorage: MSPersistentStorage = FileManagerStorage()) { self.networking = MSNetworking(session: session) self.storage = persistentStorage + self.recordingJourney = RecordingJourneyStorage.shared } // MARK: - Functions - public func fetchIsRecording() -> Bool { - return self.isRecording - } - - @discardableResult - public mutating func updateIsRecording(_ isRecording: Bool) -> Bool { - self.isRecording = isRecording - return self.isRecording - } - - public func fetchRecordingJourneyID() -> String? { - guard let recordingJourneyID = self.recordingJourneyID else { - MSLogger.make(category: .userDefaults).error("기록 중인 여정 정보를 가져오는데 실패했습니다.") - return nil - } - return recordingJourneyID - } - - public func fetchRecordingJourney(forID id: String) -> RecordingJourney? { - return self.storage.get(RecordingJourneyDTO.self, forKey: id)?.toDomain() - } - public func fetchJourneyList(userID: UUID, minCoordinate: Coordinate, maxCoordinate: Coordinate) async -> Result<[Journey], Error> { @@ -92,6 +73,14 @@ public struct JourneyRepositoryImplementation: JourneyRepository { #endif } + public func fetchRecordingJourney() -> RecordingJourney? { + guard let recordingJourneyID = self.fetchRecordingJourneyID(), + let recordingJourney = self.fetchRecordingJourney(forID: recordingJourneyID) else { + return nil + } + return recordingJourney + } + public mutating func startJourney(at coordinate: Coordinate, userID: UUID) async -> Result { #if MOCK @@ -114,21 +103,7 @@ public struct JourneyRepositoryImplementation: JourneyRepository { startTimestamp: responseDTO.startTimestamp, spots: [], coordinates: [responseDTO.coordinate.toDomain()]) - - LocalRecordingManager.shared.saveToLocal(recordingJourney.id, at: self.storage) - LocalRecordingManager.shared.saveToLocal(recordingJourney.startTimestamp, at: self.storage) - - self.recordingJourneyID = recordingJourney.id - self.isRecording = true - - #if DEBUG - if let recordingJourneyID = self.recordingJourneyID { - MSLogger.make(category: .userDefaults).debug("기록중인 여정 정보가 저장되었습니다: \(recordingJourneyID)") - } else { - MSLogger.make(category: .userDefaults).error("기록중인 여정 정보 저장에 실패했습니다.") - } - #endif - + self.recordingJourney.start(initialData: recordingJourney) return .success(recordingJourney) case .failure(let error): return .failure(error) @@ -149,11 +124,7 @@ public struct JourneyRepositoryImplementation: JourneyRepository { startTimestamp: Date(), spots: [], coordinates: coordinates) - - responseDTO.coordinates.forEach { - LocalRecordingManager.shared.saveToLocal($0, at: self.storage) - } - + self.recordingJourney.record(responseDTO.coordinates, keyPath: \.coordinates) return .success(recordingJourney) case .failure(let error): return .failure(error) @@ -170,9 +141,12 @@ public struct JourneyRepositoryImplementation: JourneyRepository { let result = await self.networking.request(EndJourneyResponseDTO.self, router: router) switch result { case .success(let responseDTO): - self.recordingJourneyID = nil - self.isRecording = false - return .success(responseDTO.id) + do { + try self.recordingJourney.finish() + return .success(responseDTO.id) + } catch { + return .failure(error) + } case .failure(let error): return .failure(error) } @@ -185,16 +159,33 @@ public struct JourneyRepositoryImplementation: JourneyRepository { let result = await self.networking.request(DeleteJourneyResponseDTO.self, router: router) switch result { case .success(let responseDTO): - self.recordingJourneyID = nil - self.isRecording = false - return .success(responseDTO.id) + do { + try self.recordingJourney.finish() + return .success(responseDTO.id) + } catch { + return .failure(error) + } case .failure(let error): return .failure(error) } } - public func loadJourneyFromLocal() -> RecordingJourney? { - return LocalRecordingManager.shared.loadJourney(from: self.storage) +} + +// MARK: - Private Functions + +private extension JourneyRepositoryImplementation { + + func fetchRecordingJourneyID() -> String? { + guard let recordingJourneyID = self.recordingJourney.id else { + MSLogger.make(category: .recordingJourneyStorage).error("기록 중인 여정 정보를 가져오는데 실패했습니다.") + return nil + } + return recordingJourneyID + } + + func fetchRecordingJourney(forID id: String) -> RecordingJourney? { + return self.storage.get(RecordingJourneyDTO.self, forKey: id)?.toDomain() } } diff --git a/iOS/MSData/Sources/MSData/RepositoryImplementation/SpotRepository.swift b/iOS/MSData/Sources/MSData/RepositoryImplementation/SpotRepository.swift index dabac2a..a6012b2 100644 --- a/iOS/MSData/Sources/MSData/RepositoryImplementation/SpotRepository.swift +++ b/iOS/MSData/Sources/MSData/RepositoryImplementation/SpotRepository.swift @@ -73,7 +73,7 @@ public struct SpotRepositoryImplementation: SpotRepository { #if DEBUG MSLogger.make(category: .network).debug("성공적으로 업로드하였습니다.") #endif - LocalRecordingManager.shared.saveToLocal(spot, at: self.storage) + RecordingJourneyStorage.shared.record([spot], keyPath: \.spots) return .success(spot.toDomain()) case .failure(let error): MSLogger.make(category: .network).error("\(error): 업로드에 실패하였습니다.") diff --git a/iOS/MSData/Sources/MSData/Util/LocalRecordingManager.swift b/iOS/MSData/Sources/MSData/Util/LocalRecordingManager.swift deleted file mode 100644 index a8eb5bf..0000000 --- a/iOS/MSData/Sources/MSData/Util/LocalRecordingManager.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// LocalRecordingManager.swift -// MSData -// -// Created by 전민건 on 2023.12.10. -// - -import Foundation - -import MSDomain -import MSLogger -import MSPersistentStorage - -// MARK: - KeyStorage - -private struct KeyStorage { - - static var id: String? - static var startTimestamp: String? - static var spots = [String]() - static var coordinates = [String]() - -} - -// MARK: - Default Implementations - -public struct LocalRecordingManager { - - public static let shared = LocalRecordingManager() - - @discardableResult - public func saveToLocal(_ value: Codable, at storage: MSPersistentStorage) -> Bool { - let key = UUID().uuidString - storage.set(value: value, forKey: key) - - switch value { - case is String: - if KeyStorage.id == nil { - KeyStorage.id = key - } else { - MSLogger.make(category: .persistable).debug("journey ID는 하나의 값만 저장할 수 있습니다.") - return false - } - case is Date: - if KeyStorage.startTimestamp == nil { - KeyStorage.startTimestamp = key - } else { - MSLogger.make(category: .persistable).debug("start timestamp는 하나의 값만 저장할 수 있습니다.") - return false - } - case is SpotDTO: - KeyStorage.spots.append(key) - case is CoordinateDTO: - KeyStorage.coordinates.append(key) - default: - MSLogger.make(category: .persistable).debug("RecordingJourney 타입의 요소들만 넣을 수 있습니다.") - return false - } - return true - } - - public func loadJourney(from storage: MSPersistentStorage) -> RecordingJourney? { - guard let id = self.loadID(from: storage), - let startTimestamp = self.loadStartTimeStamp(from: storage) else { - return nil - } - return RecordingJourney(id: id, - startTimestamp: startTimestamp, - spots: self.loadSpots(from: storage), - coordinates: self.loadCoordinates(from: storage)) - } - - private func loadStartTimeStamp(from storage: MSPersistentStorage) -> Date? { - guard let startTimestampKey = KeyStorage.startTimestamp, - let startTimestamp = storage.get(Date.self, forKey: startTimestampKey) - else { - MSLogger.make(category: .persistable).debug("id 또는 startTimestamp가 저장되지 않았습니다.") - return nil - } - return startTimestamp - } - - private func loadID(from storage: MSPersistentStorage) -> String? { - guard let idKey = KeyStorage.id, - let id = storage.get(String.self, forKey: idKey) else { - MSLogger.make(category: .persistable).debug("id 또는 startTimestamp가 저장되지 않았습니다.") - return nil - } - return id - } - - private func loadSpots(from storage: MSPersistentStorage) -> [Spot] { - return KeyStorage.spots.compactMap { spotKey in - let spotDTO = storage.get(SpotDTO.self, forKey: spotKey) - return spotDTO?.toDomain() - } - } - - private func loadCoordinates(from storage: MSPersistentStorage) -> [Coordinate] { - return KeyStorage.coordinates.compactMap { coordinateKey in - let coordinateDTO = storage.get(CoordinateDTO.self, forKey: coordinateKey) - return coordinateDTO?.toDomain() - } - } - -} diff --git a/iOS/MSData/Sources/MSData/Util/RecordingJourneyStorage.swift b/iOS/MSData/Sources/MSData/Util/RecordingJourneyStorage.swift new file mode 100644 index 0000000..aaa67d4 --- /dev/null +++ b/iOS/MSData/Sources/MSData/Util/RecordingJourneyStorage.swift @@ -0,0 +1,158 @@ +// +// RecordingJourneyStorage.swift +// MSData +// +// Created by 전민건 on 2023.12.10. +// + +import Foundation + +import MSConstants +import MSDomain +import MSLogger +import MSPersistentStorage +import MSUserDefaults + +// MARK: - Default Implementations + +public struct RecordingJourneyStorage { + + // MARK: - Properties + + private let storage: MSPersistentStorage + + @UserDefaultsWrapped(UserDefaultsKey.isRecording, defaultValue: false) + private(set) var isRecording: Bool + + @UserDefaultsWrapped(UserDefaultsKey.recordingJourneyID, defaultValue: nil) + private(set) var recordingJourneyID: String? + + /// 기록중인 여정의 ID + public var id: String? { + return self.recordingJourneyID + } + + /// 기록중인 여정 정보 + public var currentState: RecordingJourney? { + return self.fetchRecordingJourney() + } + + // MARK: - Initializer + + public init(storage: MSPersistentStorage = FileManagerStorage()) { + self.storage = storage + } + + // MARK: - Shared + + public static let shared = RecordingJourneyStorage() + + // MARK: - Functions + + public mutating func start(initialData recordingJourney: RecordingJourney) { + self.recordingJourneyID = recordingJourney.id + + if let startTimestampKey = self.key(recordingJourney.id, + forProperty: \.startTimestamp) { + self.storage.set(value: recordingJourney.startTimestamp, + forKey: startTimestampKey, + subpath: recordingJourney.id) + } else { + MSLogger.make(category: .recordingJourneyStorage) + .warning("Start Timestamp에 대한 기록이 실패했습니다.") + } + } + + @discardableResult + public func record(_ value: T, keyPath: KeyPath) -> Bool { + guard let recordingJourneyID = self.recordingJourneyID else { + MSLogger.make(category: .recordingJourneyStorage) + .error("recordingJourneyID를 조회할 수 없습니다. 여정이 기록 중인지 확인해주세요.") + return false + } + + if let key = self.key(recordingJourneyID, forProperty: keyPath) { + self.storage.set(value: value, forKey: key, subpath: recordingJourneyID) + return true + } else { + return false + } + } + + public mutating func finish() throws { + guard let recordingJourneyID = self.recordingJourneyID else { return } + try self.storage.deleteAll(subpath: recordingJourneyID) + + self.isRecording = false + self.recordingJourneyID = nil + } + +} + +// MARK: - Private Functions + +private extension RecordingJourneyStorage { + + func key(_ key: String, forProperty keyPath: KeyPath) -> String? { + switch keyPath { + case \.id: return "id" + key + case \.startTimestamp: return "ts" + key + case \.spots: return "sp" + key + case \.coordinates: return "co" + key + default: return nil + } + } + + func fetchRecordingJourney() -> RecordingJourney? { + guard let id = self.id, + let startTimestamp = self.fetchStartTimeStamp() else { + return nil + } + return RecordingJourney(id: id, + startTimestamp: startTimestamp, + spots: self.fetchSpots(), + coordinates: self.fetchCoordinates()) + } + + func fetchStartTimeStamp() -> Date? { + guard let recordingJourneyID = self.recordingJourneyID, + let startTimestampKey = self.key(recordingJourneyID, forProperty: \.startTimestamp), + let startTimestamp = self.storage.get(Date.self, + forKey: startTimestampKey, + subpath: recordingJourneyID) else { + MSLogger.make(category: .recordingJourneyStorage) + .warning("기록중인 여정에서 StartTimestamp를 불러오지 못했습니다.") + return nil + } + return startTimestamp + } + + func fetchSpots() -> [Spot] { + guard let recordingJourneyID = self.recordingJourneyID, + let spotKey = self.key(recordingJourneyID, forProperty: \.spots), + let spots = self.storage.get([SpotDTO].self, + forKey: spotKey, + subpath: recordingJourneyID) else { + MSLogger.make(category: .recordingJourneyStorage) + .warning("기록중인 여정에서 Spot 목록을 불러오지 못했습니다.") + return [] + } + + return spots.map { $0.toDomain() } + } + + func fetchCoordinates() -> [Coordinate] { + guard let recordingJourneyID = self.recordingJourneyID, + let coordinateKey = self.key(recordingJourneyID, forProperty: \.coordinates), + let coordinates = self.storage.get([CoordinateDTO].self, + forKey: coordinateKey, + subpath: recordingJourneyID) else { + MSLogger.make(category: .recordingJourneyStorage) + .warning("기록중인 여정에서 Coordinate 목록을 불러오지 못했습니다.") + return [] + } + + return coordinates.map { $0.toDomain() } + } + +} diff --git a/iOS/MSData/Tests/RepositoryTests/PersistentManagerTests.swift b/iOS/MSData/Tests/RepositoryTests/PersistentManagerTests.swift index 8bc2771..98172e6 100644 --- a/iOS/MSData/Tests/RepositoryTests/PersistentManagerTests.swift +++ b/iOS/MSData/Tests/RepositoryTests/PersistentManagerTests.swift @@ -24,11 +24,11 @@ final class PersistentManagerTests: XCTestCase { let spot = Spot(coordinate: coordinate, timestamp: .now, photoURL: url) - XCTAssertTrue(LocalRecordingManager.shared.saveToLocal(SpotDTO(spot), at: self.storage)) + XCTAssertTrue(RecordingJourneyStorage.shared.record(SpotDTO(spot), at: self.storage)) } func test_RecordingJourney_하위요소가_아닌_것들_저장_실패() { - XCTAssertFalse(LocalRecordingManager.shared.saveToLocal(Int(), at: self.storage)) + XCTAssertFalse(RecordingJourneyStorage.shared.record(Int(), at: self.storage)) } func test_RecordingJourney_반환_성공() { @@ -39,12 +39,12 @@ final class PersistentManagerTests: XCTestCase { let coordinate = Coordinate(latitude: 5, longitude: 5) let spot = Spot(coordinate: coordinate, timestamp: .now, photoURL: url) - LocalRecordingManager.shared.saveToLocal(id, at: self.storage) - LocalRecordingManager.shared.saveToLocal(Date.now, at: self.storage) - LocalRecordingManager.shared.saveToLocal(SpotDTO(spot), at: self.storage) - LocalRecordingManager.shared.saveToLocal(CoordinateDTO(coordinate), at: self.storage) + RecordingJourneyStorage.shared.record(id, at: self.storage) + RecordingJourneyStorage.shared.record(Date.now, at: self.storage) + RecordingJourneyStorage.shared.record(SpotDTO(spot), at: self.storage) + RecordingJourneyStorage.shared.record(CoordinateDTO(coordinate), at: self.storage) - guard let loadedJourney = LocalRecordingManager.shared.loadJourney(from: self.storage) else { + guard let loadedJourney = RecordingJourneyStorage.shared.loadJourney(from: self.storage) else { XCTFail("load 실패") return } diff --git a/iOS/MSDomain/Sources/MSDomain/Model/RecordingJourney.swift b/iOS/MSDomain/Sources/MSDomain/Model/RecordingJourney.swift index 167b55e..df35762 100644 --- a/iOS/MSDomain/Sources/MSDomain/Model/RecordingJourney.swift +++ b/iOS/MSDomain/Sources/MSDomain/Model/RecordingJourney.swift @@ -7,6 +7,7 @@ import Foundation +@dynamicMemberLookup public struct RecordingJourney: Identifiable { // MARK: - Properties @@ -28,6 +29,12 @@ public struct RecordingJourney: Identifiable { self.coordinates = coordinates } + // MARK: - Subscript + + subscript(dynamicMember keyPath: KeyPath) -> T { + return self[keyPath: keyPath] + } + } // MARK: - Hashable @@ -43,3 +50,18 @@ extension RecordingJourney: Hashable { } } + +// MARK: - Custom String + +extension RecordingJourney: CustomStringConvertible { + + public var description: String { + return """ + ID: \(self.id) + Starting Time: \(self.startTimestamp) + Number of Spots: \(self.spots.count) + Number of Coordinates: \(self.coordinates.count) + """ + } + +} diff --git a/iOS/MSDomain/Sources/MSDomain/Repository/JourneyRepository.swift b/iOS/MSDomain/Sources/MSDomain/Repository/JourneyRepository.swift index 5c683b5..8399375 100644 --- a/iOS/MSDomain/Sources/MSDomain/Repository/JourneyRepository.swift +++ b/iOS/MSDomain/Sources/MSDomain/Repository/JourneyRepository.swift @@ -9,16 +9,16 @@ import Foundation public protocol JourneyRepository { - func fetchIsRecording() -> Bool - mutating func updateIsRecording(_ isRecording: Bool) -> Bool - func fetchRecordingJourneyID() -> String? - func fetchRecordingJourney(forID id: String) -> RecordingJourney? + var isRecording: Bool { get } + var recordingJourneyID: String? { get } + func fetchJourneyList(userID: UUID, minCoordinate: Coordinate, maxCoordinate: Coordinate) async -> Result<[Journey], Error> + func fetchRecordingJourney() -> RecordingJourney? mutating func startJourney(at coordinate: Coordinate, userID: UUID) async -> Result mutating func endJourney(_ journey: Journey) async -> Result func recordJourney(journeyID: String, at coordinates: [Coordinate]) async -> Result mutating func deleteJourney(_ journey: RecordingJourney, userID: UUID) async -> Result - func loadJourneyFromLocal() -> RecordingJourney? + } diff --git a/iOS/MSFoundation/Sources/MSConstants/Constants.swift b/iOS/MSFoundation/Sources/MSConstants/Constants.swift index 3e7a041..55bb8c7 100644 --- a/iOS/MSFoundation/Sources/MSConstants/Constants.swift +++ b/iOS/MSFoundation/Sources/MSConstants/Constants.swift @@ -10,6 +10,6 @@ import Foundation public enum Constants { public static let appName = "MusicSpot" - public static let appBundleIdentifier = "kr.codesquad.boostcamp8.MusicSpot" + public static let appBundleIdentifier = "com.overheat.boostcamp8.MusicSpot" } diff --git a/iOS/MSFoundation/Sources/MSLogger/MSLogCategory.swift b/iOS/MSFoundation/Sources/MSLogger/MSLogCategory.swift index 5caef6c..9b5741d 100644 --- a/iOS/MSFoundation/Sources/MSLogger/MSLogCategory.swift +++ b/iOS/MSFoundation/Sources/MSLogger/MSLogCategory.swift @@ -15,7 +15,7 @@ public enum MSLogCategory: String { case userDefaults case keychain = "Keychain" case fileManager = "FileManager" - case persistable + case recordingJourneyStorage = "RecordingJourneyStorage" case music = "MusicKit" case home diff --git a/iOS/MusicSpot/MusicSpot/SceneDelegate.swift b/iOS/MusicSpot/MusicSpot/SceneDelegate.swift index da7a457..86c9ea5 100644 --- a/iOS/MusicSpot/MusicSpot/SceneDelegate.swift +++ b/iOS/MusicSpot/MusicSpot/SceneDelegate.swift @@ -8,11 +8,8 @@ import UIKit import JourneyList -import MSConstants -import MSData import MSDesignSystem import MSLogger -import MSUserDefaults class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -21,17 +18,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? private var appCoordinator: Coordinator! - @UserDefaultsWrapped(UserDefaultsKey.recordingJourneyID, defaultValue: nil) - var recordingJourneyID: String? - @UserDefaultsWrapped(UserDefaultsKey.isFirstLaunch, defaultValue: false) - var isFirstLaunch: Bool - @UserDefaultsWrapped(UserDefaultsKey.isRecording, defaultValue: false) - var isRecording: Bool - - #if DEBUG - var keychain = MSKeychainStorage() - #endif - // MARK: - Functions func scene(_ scene: UIScene, @@ -41,13 +27,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let window = UIWindow(windowScene: windowScene) defer { self.window = window } - MSLogger.make(category: .userDefaults).info("isFirstLaunch: \(self.isFirstLaunch.description)") - MSLogger.make(category: .userDefaults).info("isRecording: \(self.isRecording.description)") - MSLogger.make(category: .userDefaults).info("recordingJourneyID: \(self.recordingJourneyID ?? "No Recording")") - - #if DEBUG - self.prepareToDebug() - #endif MSFont.registerFonts() let musicSpotNavigationController = self.makeNavigationController() @@ -72,25 +51,3 @@ private extension SceneDelegate { } } - -// MARK: - Debug - -#if DEBUG -import MSKeychainStorage - -private extension SceneDelegate { - - func prepareToDebug() { - self.isFirstLaunch = true - self.recordingJourneyID = nil - self.isRecording = false - - do { - try self.keychain.deleteAll() - } catch { - MSLogger.make(category: .keychain).error("키체인 초기화 실패") - } - } - -} -#endif From e160f50dc73bcb6dd142ef8853dfe76384d5a41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Wed, 10 Jan 2024 00:53:07 +0900 Subject: [PATCH 03/20] =?UTF-8?q?:truck:=20Util=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EB=AA=85=20Storage=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MSData/{Util => Storage}/RecordingJourneyStorage.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename iOS/MSData/Sources/MSData/{Util => Storage}/RecordingJourneyStorage.swift (100%) diff --git a/iOS/MSData/Sources/MSData/Util/RecordingJourneyStorage.swift b/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift similarity index 100% rename from iOS/MSData/Sources/MSData/Util/RecordingJourneyStorage.swift rename to iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift From 092e7c65da67bef429e52368554e27ab05b936db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Wed, 10 Jan 2024 00:59:01 +0900 Subject: [PATCH 04/20] =?UTF-8?q?:sparkles:=20=EC=83=88=EB=A1=9C=EC=9A=B4?= =?UTF-8?q?=20=EC=97=AC=EC=A0=95=20=EA=B8=B0=EB=A1=9D=20=EC=8B=9C=20?= =?UTF-8?q?=EC=9D=B4=EC=A0=84=20=EA=B8=B0=EB=A1=9D=EC=9D=B4=20=EB=82=A8?= =?UTF-8?q?=EC=95=84=EC=9E=88=EB=8B=A4=EB=A9=B4=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MSData/Storage/RecordingJourneyStorage.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift b/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift index aaa67d4..c4e8167 100644 --- a/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift +++ b/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift @@ -50,6 +50,16 @@ public struct RecordingJourneyStorage { // MARK: - Functions public mutating func start(initialData recordingJourney: RecordingJourney) { + // 삭제되지 않은 이전 여정 기록이 남아있다면 삭제 + if let previousRecordingJourneyID = self.recordingJourneyID { + do { + try self.finish() + } catch { + MSLogger.make(category: .recordingJourneyStorage) + .warning("삭제되지 않은 이전 여정 기록 초기화에 실패했습니다.") + } + } + self.recordingJourneyID = recordingJourney.id if let startTimestampKey = self.key(recordingJourney.id, @@ -72,6 +82,7 @@ public struct RecordingJourneyStorage { } if let key = self.key(recordingJourneyID, forProperty: keyPath) { + // TODO: 기록중이던 Spot이나 Coordinate가 있을 경우, 이전 기록에 합쳐 저장 self.storage.set(value: value, forKey: key, subpath: recordingJourneyID) return true } else { From 2b824dd8e19d9ccc0236e87d3d1c2b42b1b70f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Wed, 10 Jan 2024 14:25:28 +0900 Subject: [PATCH 05/20] =?UTF-8?q?:sparkles:=20Spot=EA=B3=BC=20=EC=A2=8C?= =?UTF-8?q?=ED=91=9C=EA=B0=92=EB=93=A4=EC=9D=B4=20=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=EC=84=9C=20=EC=9E=91=EC=84=B1=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Storage/RecordingJourneyStorage.swift | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift b/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift index c4e8167..496c10a 100644 --- a/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift +++ b/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift @@ -51,7 +51,7 @@ public struct RecordingJourneyStorage { public mutating func start(initialData recordingJourney: RecordingJourney) { // 삭제되지 않은 이전 여정 기록이 남아있다면 삭제 - if let previousRecordingJourneyID = self.recordingJourneyID { + if self.recordingJourneyID != nil { do { try self.finish() } catch { @@ -74,7 +74,7 @@ public struct RecordingJourneyStorage { } @discardableResult - public func record(_ value: T, keyPath: KeyPath) -> Bool { + public func record(_ values: [T], keyPath: KeyPath) -> Bool { guard let recordingJourneyID = self.recordingJourneyID else { MSLogger.make(category: .recordingJourneyStorage) .error("recordingJourneyID를 조회할 수 없습니다. 여정이 기록 중인지 확인해주세요.") @@ -82,8 +82,12 @@ public struct RecordingJourneyStorage { } if let key = self.key(recordingJourneyID, forProperty: keyPath) { - // TODO: 기록중이던 Spot이나 Coordinate가 있을 경우, 이전 기록에 합쳐 저장 - self.storage.set(value: value, forKey: key, subpath: recordingJourneyID) + let recordingValues = self.makeRecordingValue(appendingValues: values, + forKey: key, + subpath: recordingJourneyID) + self.storage.set(value: recordingValues, + forKey: key, + subpath: recordingJourneyID) return true } else { return false @@ -104,12 +108,19 @@ public struct RecordingJourneyStorage { private extension RecordingJourneyStorage { + private enum Prefix { + static let idKey = "id" + static let startTimestampKey = "ts" + static let spotsKey = "sp" + static let coordinatesKey = "co" + } + func key(_ key: String, forProperty keyPath: KeyPath) -> String? { switch keyPath { - case \.id: return "id" + key - case \.startTimestamp: return "ts" + key - case \.spots: return "sp" + key - case \.coordinates: return "co" + key + case \.id: return Prefix.idKey + key + case \.startTimestamp: return Prefix.startTimestampKey + key + case \.spots: return Prefix.spotsKey + key + case \.coordinates: return Prefix.coordinatesKey + key default: return nil } } @@ -166,4 +177,16 @@ private extension RecordingJourneyStorage { return coordinates.map { $0.toDomain() } } + /// 기록 중인 이전 데이터가 남아있다면 새로운 데이터를 이전 데이터에 합칩니다. + /// 이전에 기록한 데이터가 없는 새로운 데이터라면 주어진 데이터를 그대로 반환합니다. + func makeRecordingValue(appendingValues values: [T], + forKey key: String, + subpath: String? = nil) -> [T] { + guard let recordedData = self.storage.get([T].self, forKey: key, subpath: subpath) else { + return values + } + + return recordedData + values + } + } From 5e5c715c1dc7582f649afbcd0cd3577d8cf9e5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Wed, 10 Jan 2024 15:53:43 +0900 Subject: [PATCH 06/20] =?UTF-8?q?:sparkles:=20=EC=97=AC=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20=EC=A4=91=20=EC=95=B1=20=EC=A2=85=EB=A3=8C?= =?UTF-8?q?=20=EC=8B=9C=20=EB=8B=A4=EC=9D=8C=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EB=95=8C=20=EC=9D=B4=EC=96=B4=EC=84=9C=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MapViewController+EventListener.swift | 16 +++++++ .../Common/MapViewController.swift | 48 +++++-------------- .../NavigateMap/NavigateMapViewModel.swift | 25 +++++++++- .../JourneyRepository.swift | 24 +--------- .../Storage/RecordingJourneyStorage.swift | 2 + 5 files changed, 55 insertions(+), 60 deletions(-) diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift index db87bab..3cfd8c4 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift @@ -48,6 +48,22 @@ extension MapViewController { #endif } + public func recordingShouldResume(_ recordedJourney: RecordingJourney) { + let userRepository = UserRepositoryImplementation() + let journeyRepository = JourneyRepositoryImplementation() + let recordJourneyViewModel = RecordJourneyViewModel(startedJourney: recordedJourney, + userRepository: userRepository, + journeyRepository: journeyRepository) + self.swapViewModel(to: recordJourneyViewModel) + + self.locationManager.startUpdatingLocation() + self.locationManager.allowsBackgroundLocationUpdates = true + + #if DEBUG + MSLogger.make(category: .home).debug("여정 기록이 재개되었습니다.") + #endif + } + public func recordingShouldStop(isCancelling: Bool) { guard let viewModel = self.viewModel as? RecordJourneyViewModel else { MSLogger.make(category: .home).error("여정이 종료되어야 하지만 이미 Map에서 NavigateMapViewModel을 사용하고 있습니다.") diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift index f8ea041..ae5def2 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift @@ -138,6 +138,8 @@ public final class MapViewController: UIViewController { #if DEBUG MSLogger.make(category: .home).debug("Map에 NavigateMapViewModel을 바인딩 했습니다.") #endif + + navigateMapViewModel.trigger(.viewNeedsLoaded) } if let recordJourneyViewModel = viewModel as? RecordJourneyViewModel { @@ -157,6 +159,17 @@ public final class MapViewController: UIViewController { self?.drawPolyLinesToMap(with: journeys) } .store(in: &self.cancellables) + + viewModel.state.recordingJourneyShouldResume + .sink { [weak self] recordingJourney in + self?.recordingShouldResume(recordingJourney) + + let coordinates = recordingJourney.coordinates.map { + CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) + } + self?.drawPolylineToMap(using: coordinates) + } + .store(in: &self.cancellables) } private func bind(_ viewModel: RecordJourneyViewModel) { @@ -222,24 +235,6 @@ public final class MapViewController: UIViewController { self.mapView.addAnnotation(annotation) } - public func addSpotInRecording(spot: Spot) { - Task { - - let imageFetcher = MSImageFetcher.shared - guard let photoData = await imageFetcher.fetchImage(from: spot.photoURL, - forKey: spot.photoURL.paath()) else { - throw ImageFetchError.imageFetchFailed - } - - let coordinate = CLLocationCoordinate2D(latitude: spot.coordinate.latitude, - longitude: spot.coordinate.longitude) - - self.addAnnotation(title: "", - coordinate: coordinate, - photoData: photoData) - } - } - // MARK: - Functions: Polyline private func drawPolyLinesToMap(with journeys: [Journey]) { @@ -328,15 +323,6 @@ extension MapViewController: CLLocationManagerDelegate { return } - let previousCoordinate = (self.viewModel as? RecordJourneyViewModel)?.state.previousCoordinate.value - - if let previousCoordinate = previousCoordinate { - if !self.isDistanceOver5AndUnder50(coordinate1: previousCoordinate, - coordinate2: newCurrentLocation.coordinate) { - return - } - } - let coordinate2D = CLLocationCoordinate2D(latitude: newCurrentLocation.coordinate.latitude, longitude: newCurrentLocation.coordinate.longitude) @@ -369,14 +355,6 @@ extension MapViewController: CLLocationManagerDelegate { } } - private func isDistanceOver5AndUnder50(coordinate1: CLLocationCoordinate2D, - coordinate2: CLLocationCoordinate2D) -> Bool { - let location1 = CLLocation(latitude: coordinate1.latitude, longitude: coordinate1.longitude) - let location2 = CLLocation(latitude: coordinate2.latitude, longitude: coordinate2.longitude) - MSLogger.make(category: .navigateMap).log("이동한 거리: \(location1.distance(from: location2))") - return 5 <= location1.distance(from: location2) && location1.distance(from: location2) <= 50 - } - private func presentLocationAuthorizationAlert() { let sheet = UIAlertController(title: Typo.locationAlertTitle, message: Typo.locationAlertMessage, diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/NavigateMap/NavigateMapViewModel.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/NavigateMap/NavigateMapViewModel.swift index 95012da..5b74670 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/NavigateMap/NavigateMapViewModel.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/NavigateMap/NavigateMapViewModel.swift @@ -24,7 +24,7 @@ public final class NavigateMapViewModel: MapViewModel { public struct State { // Passthrough - public var locationShouldAuthorized = PassthroughSubject() + public var recordingJourneyShouldResume = PassthroughSubject() // CurrentValue public var visibleJourneys = CurrentValueSubject<[Journey], Never>([]) @@ -47,10 +47,31 @@ public final class NavigateMapViewModel: MapViewModel { public func trigger(_ action: Action) { switch action { case .viewNeedsLoaded: - self.state.locationShouldAuthorized.send(true) + if let recordingJourney = self.fetchRecordingJourneyIfNeeded() { + #if DEBUG + MSLogger.make(category: .navigateMap) + .debug("기록중이던 여정이 발견되었습니다: \(recordingJourney)") + #endif + self.state.recordingJourneyShouldResume.send(recordingJourney) + } case .visibleJourneysDidUpdated(let visibleJourneys): self.state.visibleJourneys.send(visibleJourneys) } } } + +// MARK: - Private Functions + +private extension NavigateMapViewModel { + + /// 앱 종료 전 진행중이던 여정 기록이 남아있는지 확인합니다. + /// 진행 중이던 여정 기록이 있다면 해당 데이터를 불러옵니다. + /// - Returns: 진행 중이던 여정 기록. 없다면 `nil`을 반환합니다. + func fetchRecordingJourneyIfNeeded() -> RecordingJourney? { + guard self.journeyRepository.isRecording else { return nil } + + return self.journeyRepository.fetchRecordingJourney() + } + +} diff --git a/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift b/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift index 259d1b3..7d3c8c3 100644 --- a/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift +++ b/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift @@ -74,11 +74,7 @@ public struct JourneyRepositoryImplementation: JourneyRepository { } public func fetchRecordingJourney() -> RecordingJourney? { - guard let recordingJourneyID = self.fetchRecordingJourneyID(), - let recordingJourney = self.fetchRecordingJourney(forID: recordingJourneyID) else { - return nil - } - return recordingJourney + return self.recordingJourney.currentState } public mutating func startJourney(at coordinate: Coordinate, @@ -171,21 +167,3 @@ public struct JourneyRepositoryImplementation: JourneyRepository { } } - -// MARK: - Private Functions - -private extension JourneyRepositoryImplementation { - - func fetchRecordingJourneyID() -> String? { - guard let recordingJourneyID = self.recordingJourney.id else { - MSLogger.make(category: .recordingJourneyStorage).error("기록 중인 여정 정보를 가져오는데 실패했습니다.") - return nil - } - return recordingJourneyID - } - - func fetchRecordingJourney(forID id: String) -> RecordingJourney? { - return self.storage.get(RecordingJourneyDTO.self, forKey: id)?.toDomain() - } - -} diff --git a/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift b/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift index 496c10a..456e1ac 100644 --- a/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift +++ b/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift @@ -50,6 +50,8 @@ public struct RecordingJourneyStorage { // MARK: - Functions public mutating func start(initialData recordingJourney: RecordingJourney) { + defer { self.isRecording = true } + // 삭제되지 않은 이전 여정 기록이 남아있다면 삭제 if self.recordingJourneyID != nil { do { From 9d52ea1bbc1aa83b2d92604207786c6d3c9a2443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Wed, 10 Jan 2024 15:54:30 +0900 Subject: [PATCH 07/20] =?UTF-8?q?:rocket:=200.6.2=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj b/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj index 6a576a6..0f66778 100644 --- a/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj +++ b/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -402,7 +402,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.6.0; + MARKETING_VERSION = 0.6.2; PRODUCT_BUNDLE_IDENTIFIER = com.overheat.boostcamp8.MusicSpot; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -447,7 +447,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.6.0; + MARKETING_VERSION = 0.6.2; PRODUCT_BUNDLE_IDENTIFIER = com.overheat.boostcamp8.MusicSpot; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -556,7 +556,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.6.0; + MARKETING_VERSION = 0.6.2; PRODUCT_BUNDLE_IDENTIFIER = com.overheat.boostcamp8.MusicSpot; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 34098939609549070ce2ff2958778f54fb711466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Wed, 10 Jan 2024 16:43:01 +0900 Subject: [PATCH 08/20] =?UTF-8?q?:bug:=20Spot=20=EC=99=84=EB=A3=8C=20Navig?= =?UTF-8?q?ation=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/SaveSpotViewController.swift | 8 +++----- .../Spot/Presentation/SaveSpotViewModel.swift | 15 ++++++++------- .../Spot/Presentation/SpotViewController.swift | 8 +------- .../Spot/Presentation/SpotViewModel.swift | 4 +--- .../MusicSpot/Coordinator/SpotCoordinator.swift | 16 ++++++++++------ 5 files changed, 23 insertions(+), 28 deletions(-) diff --git a/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewController.swift b/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewController.swift index 77f9e50..f6a882e 100644 --- a/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewController.swift +++ b/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewController.swift @@ -114,10 +114,9 @@ public final class SaveSpotViewController: UIViewController { // MARK: - Combine Binding private func bind() { - self.viewModel.state.spot + self.viewModel.state.uploadedSpot .receive(on: DispatchQueue.main) .sink { [weak self] spot in - guard let spot = spot else { return } self?.navigationDelegate?.popToHome(spot: spot) } .store(in: &self.cancellables) @@ -262,12 +261,11 @@ public final class SaveSpotViewController: UIViewController { private func completeButtonDidTap() { guard let jpegData = self.image.jpegData(compressionQuality: 0.1) else { - MSLogger.make(category: .spot).debug("현재 이미지를 Data로 변환할 수 없습니다.") + MSLogger.make(category: .spot).warning("현재 이미지를 Data로 변환할 수 없습니다.") return } - self.viewModel.trigger(.startUploadSpot(jpegData)) - self.navigationDelegate?.popToHome() + self.viewModel.trigger(.uploadSpot(jpegData)) } } diff --git a/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewModel.swift b/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewModel.swift index 941d19b..6db1c41 100644 --- a/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewModel.swift +++ b/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewModel.swift @@ -15,11 +15,12 @@ import MSLogger public final class SaveSpotViewModel { public enum Action { - case startUploadSpot(Data) + case uploadSpot(Data) } public struct State { - public var spot = PassthroughSubject() + // Passthrough + public var uploadedSpot = PassthroughSubject() } // MARK: - Properties @@ -51,21 +52,21 @@ internal extension SaveSpotViewModel { func trigger(_ action: Action) { switch action { - case .startUploadSpot(let data): + case .uploadSpot(let data): Task { guard let recordingJourneyID = self.journeyRepository.recordingJourneyID else { MSLogger.make(category: .spot).error("recoding 중인 journeyID를 찾지 못하였습니다.") return } let spot = RequestableSpot(journeyID: recordingJourneyID, - coordinate: self.coordinate, - timestamp: .now, - photoData: data) + coordinate: self.coordinate, + timestamp: .now, + photoData: data) let result = await self.spotRepository.upload(spot: spot) switch result { case .success(let spot): - self.state.spot.send(spot) + self.state.uploadedSpot.send(spot) MSLogger.make(category: .network).debug("성공적으로 업로드되었습니다: \(spot)") case .failure(let error): MSLogger.make(category: .network).error("\(error): 업로드에 실패하였습니다.") diff --git a/iOS/Features/Spot/Sources/Spot/Presentation/SpotViewController.swift b/iOS/Features/Spot/Sources/Spot/Presentation/SpotViewController.swift index bf6bf73..833421d 100644 --- a/iOS/Features/Spot/Sources/Spot/Presentation/SpotViewController.swift +++ b/iOS/Features/Spot/Sources/Spot/Presentation/SpotViewController.swift @@ -351,13 +351,7 @@ extension SpotViewController: UIImagePickerControllerDelegate { self.presentSpotSaveViewController(with: image, coordinate: self.viewModel.coordinate) } -} - -// MARK: - Functions - -private extension SpotViewController { - - func presentSpotSaveViewController(with image: UIImage, coordinate: Coordinate) { + private func presentSpotSaveViewController(with image: UIImage, coordinate: Coordinate) { self.viewModel.stopCamera() self.navigationDelegate?.presentSaveSpot(using: image, coordinate: coordinate) } diff --git a/iOS/Features/Spot/Sources/Spot/Presentation/SpotViewModel.swift b/iOS/Features/Spot/Sources/Spot/Presentation/SpotViewModel.swift index ce2d960..d4a6dc9 100644 --- a/iOS/Features/Spot/Sources/Spot/Presentation/SpotViewModel.swift +++ b/iOS/Features/Spot/Sources/Spot/Presentation/SpotViewModel.swift @@ -50,9 +50,7 @@ public final class SpotViewModel: NSObject { weak var delegate: ShotDelegate? var swapMode: SwapMode = .back { - didSet { - self.configureSwapMode() - } + didSet { self.configureSwapMode() } } private let session = AVCaptureSession() diff --git a/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift b/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift index 3f6cd34..95a45af 100644 --- a/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift +++ b/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift @@ -51,7 +51,7 @@ extension SpotCoordinator: SpotNavigationDelegate { picker.sourceType = .photoLibrary picker.allowsEditing = true picker.delegate = spotViewController - spotViewController.present(picker, animated: true) + self.navigationController.present(picker, animated: true) } func presentSaveSpot(using image: UIImage, coordinate: Coordinate) { @@ -61,10 +61,12 @@ extension SpotCoordinator: SpotNavigationDelegate { spotRepository: spotRepository, coordinate: coordinate) let spotSaveViewController = SaveSpotViewController(image: image, viewModel: viewModel) - spotSaveViewController.modalPresentationStyle = .fullScreen + spotSaveViewController.modalPresentationStyle = .overFullScreen spotSaveViewController.navigationDelegate = self - self.navigationController.presentedViewController?.dismiss(animated: true) - self.navigationController.present(spotSaveViewController, animated: true) + + self.navigationController.presentedViewController?.dismiss(animated: true) { [weak self] in + self?.navigationController.present(spotSaveViewController, animated: true) + } } func dismissToSpot() { @@ -76,8 +78,10 @@ extension SpotCoordinator: SpotNavigationDelegate { spotSaveViewController.dismiss(animated: true) } - func popToHome(spot: Spot? = nil) { - self.finish() + func popToHome(spot: Spot?) { + self.navigationController.presentedViewController?.dismiss(animated: true) { [weak self] in + self?.finish() + } } } From 0390082cd400c1988a3752252c1278501d354db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Wed, 10 Jan 2024 17:56:19 +0900 Subject: [PATCH 09/20] =?UTF-8?q?:sparkles:=20=EC=97=AC=EA=B8=B0=EC=84=9C?= =?UTF-8?q?=20=EB=8B=A4=EC=8B=9C=20=EA=B2=80=EC=83=89=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EC=9D=B4=20=EC=97=AC=EC=A0=95=20=EA=B8=B0=EB=A1=9D=20=EC=A4=91?= =?UTF-8?q?=EC=97=90=20=EB=B3=B4=EC=97=AC=EC=A7=80=EB=8D=98=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Sources/Home/Presentation/HomeViewController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift index b41085a..4eddc51 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift @@ -170,9 +170,10 @@ public final class HomeViewController: HomeBottomSheetViewController { self.viewModel.state.isRefreshButtonHidden .removeDuplicates(by: { $0 == $1 }) + .combineLatest(self.viewModel.state.isRecording) .receive(on: DispatchQueue.main) - .sink { [weak self] isHidden in - self?.refreshButton.isHidden = isHidden + .sink { [weak self] isHidden, isRecording in + self?.refreshButton.isHidden = (isHidden && !isRecording) } .store(in: &self.cancellables) From 787b2cb2423f40665663726b3b9a05e78de0b5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Wed, 10 Jan 2024 20:32:59 +0900 Subject: [PATCH 10/20] =?UTF-8?q?:recycle:=20=EC=97=AC=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=20=ED=8C=8C?= =?UTF-8?q?=ED=8E=B8=ED=99=94=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 여러 곳에 파편화되어 있던 여정 시작 / 중단 / 재개 로직을 Home 부분에 통합 --- .../Presentation/HomeViewController.swift | 52 +++++------ .../Home/Presentation/HomeViewModel.swift | 88 ++++++++++++------- .../MapViewController+EventListener.swift | 25 +++--- .../Common/MapViewController.swift | 46 ++++------ .../NavigateMap/NavigateMapViewModel.swift | 31 ------- .../RecordJourneyViewModel.swift | 22 ----- .../Journey/DeleteJourneyResponseDTO.swift | 15 ++++ .../JourneyRepository.swift | 5 +- .../Repository/JourneyRepository.swift | 4 +- 9 files changed, 133 insertions(+), 155 deletions(-) diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift index 4eddc51..bc65e6b 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift @@ -116,12 +116,6 @@ public final class HomeViewController: HomeBottomSheetViewController { self.navigationController?.isNavigationBarHidden = true } - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - self.viewModel.trigger(.viewNeedsReloaded) - } - public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -132,10 +126,27 @@ 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 { [weak self] cancelledJourney in + self?.contentViewController.clearOverlays() + self?.contentViewController.recordingDidStop(cancelledJourney) } .store(in: &self.cancellables) @@ -155,7 +166,6 @@ public final class HomeViewController: HomeBottomSheetViewController { self?.hideBottomSheet() } else { self?.showBottomSheet() - self?.contentViewController.recordingShouldStop(isCancelling: false) } self?.updateButtonMode(isRecording: isRecording) } @@ -169,19 +179,15 @@ public final class HomeViewController: HomeBottomSheetViewController { .store(in: &self.cancellables) self.viewModel.state.isRefreshButtonHidden - .removeDuplicates(by: { $0 == $1 }) .combineLatest(self.viewModel.state.isRecording) .receive(on: DispatchQueue.main) .sink { [weak self] isHidden, isRecording in - self?.refreshButton.isHidden = (isHidden && !isRecording) - } - .store(in: &self.cancellables) - - self.viewModel.state.overlaysShouldBeCleared - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.contentViewController.clearOverlays() - self?.contentViewController.clearAnnotations() + 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) } @@ -189,14 +195,11 @@ public final class HomeViewController: HomeBottomSheetViewController { // MARK: - Functions private func updateButtonMode(isRecording: Bool) { - UIView.transition(with: startButton, duration: 0.5, + UIView.transition(with: self.view, + duration: 0.5, 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 }) } @@ -236,7 +239,6 @@ extension HomeViewController: RecordJourneyButtonViewDelegate { guard self.viewModel.state.isRecording.value == true else { return } self.viewModel.trigger(.backButtonDidTap) - self.contentViewController.recordingShouldStop(isCancelling: true) } public func spotButtonDidTap(_ button: MSRectButton) { diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift index 0912b8d..86be724 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift @@ -20,18 +20,19 @@ public final class HomeViewModel { public enum Action { case viewNeedsLoaded - case viewNeedsReloaded 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() + public var journeyDidStarted = PassthroughSubject() + public var journeyDidResumed = PassthroughSubject() + public var journeyDidCancelled = PassthroughSubject() public var visibleJourneys = PassthroughSubject<[Journey], Never>() - public var overlaysShouldBeCleared = PassthroughSubject() // CurrentValue public var isRecording = CurrentValueSubject(false) @@ -68,28 +69,23 @@ public final class HomeViewModel { #endif self.createNewUserWhenFirstLaunch() - case .viewNeedsReloaded: - let isRecording = self.journeyRepository.isRecording - #if DEBUG - MSLogger.make(category: .home).debug("여정 기록 중 여부: \(isRecording)") - #endif - if isRecording { - self.resumeJourney() - } - self.state.isRecording.send(isRecording) + + self.resumeJourneyIfNeeded() 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) @@ -124,8 +120,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) } @@ -134,36 +148,46 @@ 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)") } } } - func resumeJourney() { - guard let recordingJourney = self.journeyRepository.fetchRecordingJourney() else { - return - } - - MSLogger.make(category: .home).debug("Recording Journey: \(recordingJourney)") + func syncRecordingState() { + let isRecording = self.journeyRepository.isRecording + self.state.isRecording.send(isRecording) } } diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift index 3cfd8c4..36644e4 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift @@ -5,6 +5,7 @@ // Created by 이창준 on 2023.12.10. // +import CoreLocation import Foundation import MSData @@ -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 @@ -44,11 +45,11 @@ extension MapViewController { self.locationManager.allowsBackgroundLocationUpdates = true #if DEBUG - MSLogger.make(category: .home).debug("여정 기록이 시작되었습니다.") + MSLogger.make(category: .home).debug("여정 기록이 시작되었습니다: \(startedJourney)") #endif } - public func recordingShouldResume(_ recordedJourney: RecordingJourney) { + public func recordingDidResume(_ recordedJourney: RecordingJourney) { let userRepository = UserRepositoryImplementation() let journeyRepository = JourneyRepositoryImplementation() let recordJourneyViewModel = RecordJourneyViewModel(startedJourney: recordedJourney, @@ -56,24 +57,26 @@ extension MapViewController { 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 DEBUG - MSLogger.make(category: .home).debug("여정 기록이 재개되었습니다.") + MSLogger.make(category: .home).debug("여정 기록이 재개되었습니다: \(recordedJourney)") #endif } - public func recordingShouldStop(isCancelling: Bool) { - guard let viewModel = self.viewModel as? RecordJourneyViewModel else { + public func recordingDidStop(_ stoppedJourney: RecordingJourney) { + guard self.viewModel is RecordJourneyViewModel else { MSLogger.make(category: .home).error("여정이 종료되어야 하지만 이미 Map에서 NavigateMapViewModel을 사용하고 있습니다.") return } - if isCancelling { - viewModel.trigger(.recordingDidCancelled) - } - let journeyRepository = JourneyRepositoryImplementation() let navigateMapViewModel = NavigateMapViewModel(repository: journeyRepository) self.swapViewModel(to: navigateMapViewModel) @@ -82,7 +85,7 @@ extension MapViewController { self.locationManager.allowsBackgroundLocationUpdates = false #if DEBUG - MSLogger.make(category: .home).debug("여정 기록이 종료되었습니다.") + MSLogger.make(category: .home).debug("여정 기록이 종료되었습니다: \(stoppedJourney)") #endif } diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift index ae5def2..a7bce5b 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift @@ -135,39 +135,26 @@ public final class MapViewController: UIViewController { if let navigateMapViewModel = viewModel as? NavigateMapViewModel { self.bind(navigateMapViewModel) - #if DEBUG MSLogger.make(category: .home).debug("Map에 NavigateMapViewModel을 바인딩 했습니다.") - #endif - - navigateMapViewModel.trigger(.viewNeedsLoaded) + return } if let recordJourneyViewModel = viewModel as? RecordJourneyViewModel { self.bind(recordJourneyViewModel) - #if DEBUG MSLogger.make(category: .home).debug("Map에 RecordJourneyViewModel을 바인딩 했습니다.") - #endif + return } + + MSLogger.make(category: .home).warning("Map에 ViewModel을 바인딩하지 못했습니다.") } private func bind(_ viewModel: NavigateMapViewModel) { viewModel.state.visibleJourneys .receive(on: DispatchQueue.main) .sink { [weak self] journeys in - self?.clearAnnotations() + self?.clearOverlays() self?.addAnnotations(with: journeys) - self?.drawPolyLinesToMap(with: journeys) - } - .store(in: &self.cancellables) - - viewModel.state.recordingJourneyShouldResume - .sink { [weak self] recordingJourney in - self?.recordingShouldResume(recordingJourney) - - let coordinates = recordingJourney.coordinates.map { - CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) - } - self?.drawPolylineToMap(using: coordinates) + self?.drawJourneyListPolylines(with: journeys) } .store(in: &self.cancellables) } @@ -185,7 +172,7 @@ public final class MapViewController: UIViewController { .receive(on: DispatchQueue.main) .sink { [weak self] previousCoordinate, currentCoordinate in let points = [previousCoordinate, currentCoordinate] - self?.drawPolylineToMap(using: points) + self?.drawPolyline(using: points) } .store(in: &self.cancellables) } @@ -237,7 +224,7 @@ public final class MapViewController: UIViewController { // MARK: - Functions: Polyline - private func drawPolyLinesToMap(with journeys: [Journey]) { + func drawJourneyListPolylines(with journeys: [Journey]) { Task { await withTaskGroup(of: Void.self) { group in for journey in journeys { @@ -246,31 +233,30 @@ public final class MapViewController: UIViewController { CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) } - await self.drawPolylineToMap(using: coordinates) + await self.drawPolyline(using: coordinates) } } } } } - private func drawPolylineToMap(using coordinates: [CLLocationCoordinate2D]) { - let polyline = MKPolyline(coordinates: coordinates, - count: coordinates.count) + func drawPolyline(using coordinates: [CLLocationCoordinate2D]) { + let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count) self.mapView.addOverlay(polyline) } // MARK: - Functions - public func clearOverlays() { - let overlays = self.mapView.overlays - self.mapView.removeOverlays(overlays) - } - public func clearAnnotations() { let annotations = self.mapView.annotations self.mapView.removeAnnotations(annotations) } + public func clearOverlays() { + let overlays = self.mapView.overlays + self.mapView.removeOverlays(overlays) + } + } // MARK: - UI Configuration diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/NavigateMap/NavigateMapViewModel.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/NavigateMap/NavigateMapViewModel.swift index 5b74670..26d2859 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/NavigateMap/NavigateMapViewModel.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/NavigateMap/NavigateMapViewModel.swift @@ -6,26 +6,18 @@ // import Combine -import CoreLocation import Foundation -import MSConstants -import MSData import MSDomain import MSLogger -import MSUserDefaults public final class NavigateMapViewModel: MapViewModel { public enum Action { - case viewNeedsLoaded case visibleJourneysDidUpdated(_ visibleJourneys: [Journey]) } public struct State { - // Passthrough - public var recordingJourneyShouldResume = PassthroughSubject() - // CurrentValue public var visibleJourneys = CurrentValueSubject<[Journey], Never>([]) } @@ -46,32 +38,9 @@ public final class NavigateMapViewModel: MapViewModel { public func trigger(_ action: Action) { switch action { - case .viewNeedsLoaded: - if let recordingJourney = self.fetchRecordingJourneyIfNeeded() { - #if DEBUG - MSLogger.make(category: .navigateMap) - .debug("기록중이던 여정이 발견되었습니다: \(recordingJourney)") - #endif - self.state.recordingJourneyShouldResume.send(recordingJourney) - } case .visibleJourneysDidUpdated(let visibleJourneys): self.state.visibleJourneys.send(visibleJourneys) } } } - -// MARK: - Private Functions - -private extension NavigateMapViewModel { - - /// 앱 종료 전 진행중이던 여정 기록이 남아있는지 확인합니다. - /// 진행 중이던 여정 기록이 있다면 해당 데이터를 불러옵니다. - /// - Returns: 진행 중이던 여정 기록. 없다면 `nil`을 반환합니다. - func fetchRecordingJourneyIfNeeded() -> RecordingJourney? { - guard self.journeyRepository.isRecording else { return nil } - - return self.journeyRepository.fetchRecordingJourney() - } - -} diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/RecordJourney/RecordJourneyViewModel.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/RecordJourney/RecordJourneyViewModel.swift index 1d4e0f7..34b5039 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/RecordJourney/RecordJourneyViewModel.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/RecordJourney/RecordJourneyViewModel.swift @@ -9,17 +9,14 @@ import Combine import CoreLocation import Foundation -import MSData import MSDomain import MSLogger public final class RecordJourneyViewModel: MapViewModel { public enum Action { - case viewNeedsLoaded case locationDidUpdated(CLLocationCoordinate2D) case locationsShouldRecorded([CLLocationCoordinate2D]) - case recordingDidCancelled } public struct State { @@ -50,10 +47,6 @@ public final class RecordJourneyViewModel: MapViewModel { public func trigger(_ action: Action) { switch action { - case .viewNeedsLoaded: - #if DEBUG - MSLogger.make(category: .home).debug("View Did load.") - #endif case .locationDidUpdated(let coordinate): let previousCoordinate = self.state.currentCoordinate.value self.state.previousCoordinate.send(previousCoordinate) @@ -71,21 +64,6 @@ public final class RecordJourneyViewModel: MapViewModel { MSLogger.make(category: .home).error("\(error)") } } - case .recordingDidCancelled: - Task { - guard let userID = self.userRepository.fetchUUID() else { return } - - let recordingJourney = self.state.recordingJourney.value - let result = await self.journeyRepository.deleteJourney(recordingJourney, userID: userID) - switch result { - case .success(let cancelledJourney): - #if DEBUG - MSLogger.make(category: .home).debug("여정이 취소 되었습니다: \(cancelledJourney)") - #endif - case .failure(let error): - MSLogger.make(category: .home).error("\(error)") - } - } } } diff --git a/iOS/MSData/Sources/MSData/DTO/Response/Journey/DeleteJourneyResponseDTO.swift b/iOS/MSData/Sources/MSData/DTO/Response/Journey/DeleteJourneyResponseDTO.swift index 52d942f..aee910e 100644 --- a/iOS/MSData/Sources/MSData/DTO/Response/Journey/DeleteJourneyResponseDTO.swift +++ b/iOS/MSData/Sources/MSData/DTO/Response/Journey/DeleteJourneyResponseDTO.swift @@ -42,3 +42,18 @@ extension DeleteJourneyResponseDTO: Decodable { } } + +// MARK: - Domain Mapping + +import MSDomain + +extension DeleteJourneyResponseDTO { + + public func toDomain() -> RecordingJourney { + return RecordingJourney(id: self.id, + startTimestamp: self.metadata.startTimestamp, + spots: self.spots.map { $0.toDomain() }, + coordinates: self.coordinates.map { $0.toDomain() }) + } + +} diff --git a/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift b/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift index 7d3c8c3..ffe90ef 100644 --- a/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift +++ b/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift @@ -149,7 +149,7 @@ public struct JourneyRepositoryImplementation: JourneyRepository { } public mutating func deleteJourney(_ recordingJourney: RecordingJourney, - userID: UUID) async -> Result { + userID: UUID) async -> Result { let requestDTO = DeleteJourneyRequestDTO(userID: userID, journeyID: recordingJourney.id) let router = JourneyRouter.deleteJourney(dto: requestDTO) let result = await self.networking.request(DeleteJourneyResponseDTO.self, router: router) @@ -157,7 +157,8 @@ public struct JourneyRepositoryImplementation: JourneyRepository { case .success(let responseDTO): do { try self.recordingJourney.finish() - return .success(responseDTO.id) + let deletedJourney = responseDTO.toDomain() + return .success(deletedJourney) } catch { return .failure(error) } diff --git a/iOS/MSDomain/Sources/MSDomain/Repository/JourneyRepository.swift b/iOS/MSDomain/Sources/MSDomain/Repository/JourneyRepository.swift index 8399375..a5b037a 100644 --- a/iOS/MSDomain/Sources/MSDomain/Repository/JourneyRepository.swift +++ b/iOS/MSDomain/Sources/MSDomain/Repository/JourneyRepository.swift @@ -17,8 +17,8 @@ public protocol JourneyRepository { maxCoordinate: Coordinate) async -> Result<[Journey], Error> func fetchRecordingJourney() -> RecordingJourney? mutating func startJourney(at coordinate: Coordinate, userID: UUID) async -> Result - mutating func endJourney(_ journey: Journey) async -> Result func recordJourney(journeyID: String, at coordinates: [Coordinate]) async -> Result - mutating func deleteJourney(_ journey: RecordingJourney, userID: UUID) async -> Result + mutating func endJourney(_ journey: Journey) async -> Result + mutating func deleteJourney(_ journey: RecordingJourney, userID: UUID) async -> Result } From 20c1280293c947cf192da333b31e2263075bdfa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Wed, 10 Jan 2024 20:34:39 +0900 Subject: [PATCH 11/20] =?UTF-8?q?:rocket:=20=EB=B9=8C=EB=93=9C=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=202=EB=A1=9C=20=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj b/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj index 0f66778..90a853d 100644 --- a/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj +++ b/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj @@ -380,7 +380,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = XW996HTK32; GENERATE_INFOPLIST_FILE = YES; @@ -425,7 +425,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = XW996HTK32; GENERATE_INFOPLIST_FILE = YES; @@ -534,7 +534,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = XW996HTK32; GENERATE_INFOPLIST_FILE = YES; From bae2e0d75d1d40d1bf6f913d3de4bc947b1b018e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Wed, 10 Jan 2024 20:53:04 +0900 Subject: [PATCH 12/20] =?UTF-8?q?:test=5Ftube:=20RecordingJourneyStorage?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9C=A0?= =?UTF-8?q?=EB=8B=9B=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PersistentManagerTests.swift | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/iOS/MSData/Tests/RepositoryTests/PersistentManagerTests.swift b/iOS/MSData/Tests/RepositoryTests/PersistentManagerTests.swift index 98172e6..97535c9 100644 --- a/iOS/MSData/Tests/RepositoryTests/PersistentManagerTests.swift +++ b/iOS/MSData/Tests/RepositoryTests/PersistentManagerTests.swift @@ -14,7 +14,12 @@ final class PersistentManagerTests: XCTestCase { // MARK: - Properties - let storage = FileManagerStorage() + private let storage = FileManagerStorage() + private var recordingJourney = RecordingJourneyStorage.shared + + override func tearDown() async throws { + try self.recordingJourney.finish() + } // MARK: - Tests @@ -22,13 +27,13 @@ final class PersistentManagerTests: XCTestCase { let coordinate = Coordinate(latitude: 10, longitude: 10) let url = URL(string: "/../")! - let spot = Spot(coordinate: coordinate, timestamp: .now, photoURL: url) + self.recordingJourney.start(initialData: RecordingJourney(id: UUID().uuidString, + startTimestamp: .now, + spots: [], + coordinates: [])) - XCTAssertTrue(RecordingJourneyStorage.shared.record(SpotDTO(spot), at: self.storage)) - } - - func test_RecordingJourney_하위요소가_아닌_것들_저장_실패() { - XCTAssertFalse(RecordingJourneyStorage.shared.record(Int(), at: self.storage)) + let spot = Spot(coordinate: coordinate, timestamp: .now, photoURL: url) + XCTAssertTrue(self.recordingJourney.record([SpotDTO(spot)], keyPath: \.spots)) } func test_RecordingJourney_반환_성공() { @@ -39,12 +44,14 @@ final class PersistentManagerTests: XCTestCase { let coordinate = Coordinate(latitude: 5, longitude: 5) let spot = Spot(coordinate: coordinate, timestamp: .now, photoURL: url) - RecordingJourneyStorage.shared.record(id, at: self.storage) - RecordingJourneyStorage.shared.record(Date.now, at: self.storage) - RecordingJourneyStorage.shared.record(SpotDTO(spot), at: self.storage) - RecordingJourneyStorage.shared.record(CoordinateDTO(coordinate), at: self.storage) + self.recordingJourney.start(initialData: RecordingJourney(id: id, + startTimestamp: startTimestamp, + spots: [], + coordinates: [])) + self.recordingJourney.record([SpotDTO(spot)], keyPath: \.spots) + self.recordingJourney.record([CoordinateDTO(coordinate)], keyPath: \.coordinates) - guard let loadedJourney = RecordingJourneyStorage.shared.loadJourney(from: self.storage) else { + guard let loadedJourney = self.recordingJourney.currentState else { XCTFail("load 실패") return } From 5a0c3a4f15cd97ea971b5f841fabdb6e5d1802b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Thu, 11 Jan 2024 00:16:46 +0900 Subject: [PATCH 13/20] =?UTF-8?q?:bug:=20Spot=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=9D=B4=20close=20=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift b/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift index 95a45af..11942e0 100644 --- a/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift +++ b/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift @@ -79,8 +79,12 @@ extension SpotCoordinator: SpotNavigationDelegate { } func popToHome(spot: Spot?) { - self.navigationController.presentedViewController?.dismiss(animated: true) { [weak self] in - self?.finish() + if spot != nil { + self.navigationController.presentedViewController?.dismiss(animated: true) { [weak self] in + self?.finish() + } + } else { + self.finish() } } From 26ccb3bd296ebcde6d689d8de2ab3d21518daac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Thu, 11 Jan 2024 00:28:50 +0900 Subject: [PATCH 14/20] =?UTF-8?q?:bug:=20Spot=20=EC=B9=B4=EB=A9=94?= =?UTF-8?q?=EB=9D=BC=20=EC=82=AC=EC=9A=A9=20=EC=8B=9C=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MusicSpot/Coordinator/SpotCoordinator.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift b/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift index 11942e0..84eb1fa 100644 --- a/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift +++ b/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift @@ -61,11 +61,15 @@ extension SpotCoordinator: SpotNavigationDelegate { spotRepository: spotRepository, coordinate: coordinate) let spotSaveViewController = SaveSpotViewController(image: image, viewModel: viewModel) - spotSaveViewController.modalPresentationStyle = .overFullScreen + spotSaveViewController.modalPresentationStyle = .fullScreen spotSaveViewController.navigationDelegate = self - self.navigationController.presentedViewController?.dismiss(animated: true) { [weak self] in - self?.navigationController.present(spotSaveViewController, animated: true) + if let presentedViewController = self.navigationController.presentedViewController { + presentedViewController.dismiss(animated: true) { [weak self] in + self?.navigationController.present(spotSaveViewController, animated: true) + } + } else { + self.navigationController.present(spotSaveViewController, animated: true) } } @@ -79,8 +83,8 @@ extension SpotCoordinator: SpotNavigationDelegate { } func popToHome(spot: Spot?) { - if spot != nil { - self.navigationController.presentedViewController?.dismiss(animated: true) { [weak self] in + if let presentedViewController = self.navigationController.presentedViewController { + presentedViewController.dismiss(animated: true) { [weak self] in self?.finish() } } else { From a0b8013234ef6cac7271fa8b0f1adf5d76c1048a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Thu, 11 Jan 2024 00:31:25 +0900 Subject: [PATCH 15/20] =?UTF-8?q?:rocket:=200.6.2(3)=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=20=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj b/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj index 90a853d..7daa0d7 100644 --- a/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj +++ b/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj @@ -380,7 +380,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = XW996HTK32; GENERATE_INFOPLIST_FILE = YES; @@ -425,7 +425,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = XW996HTK32; GENERATE_INFOPLIST_FILE = YES; @@ -534,7 +534,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = XW996HTK32; GENERATE_INFOPLIST_FILE = YES; From 8f04848ed068ff5c1cb60647ce0d173b3ff49a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Thu, 11 Jan 2024 00:51:08 +0900 Subject: [PATCH 16/20] =?UTF-8?q?:bug:=20=EC=97=AC=EC=A0=95=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EB=84=A4=ED=8A=B8=EC=9B=8C=ED=82=B9=EC=9D=B4=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=ED=95=9C=20=EA=B2=BD=EC=9A=B0=EC=97=90?= =?UTF-8?q?=EB=8F=84=20=EB=8F=99=EC=9E=91=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Presentation/HomeViewController.swift | 11 ++++++++--- .../Sources/Home/Presentation/HomeViewModel.swift | 3 ++- .../Common/MapViewController+EventListener.swift | 6 ++++-- .../RepositoryImplementation/JourneyRepository.swift | 12 +++++------- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift index bc65e6b..1bf3e11 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift @@ -144,10 +144,15 @@ public final class HomeViewController: HomeBottomSheetViewController { self.viewModel.state.journeyDidCancelled .receive(on: DispatchQueue.main) - .sink { [weak self] cancelledJourney in + .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 @@ -196,7 +201,7 @@ public final class HomeViewController: HomeBottomSheetViewController { private func updateButtonMode(isRecording: Bool) { UIView.transition(with: self.view, - duration: 0.5, + duration: 0.34, options: .transitionCrossDissolve, animations: { [weak self] in self?.startButton.isHidden = isRecording diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift index 86be724..052cf92 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift @@ -31,7 +31,7 @@ public final class HomeViewModel { // Passthrough public var journeyDidStarted = PassthroughSubject() public var journeyDidResumed = PassthroughSubject() - public var journeyDidCancelled = PassthroughSubject() + public var journeyDidCancelled = PassthroughSubject() public var visibleJourneys = PassthroughSubject<[Journey], Never>() // CurrentValue @@ -181,6 +181,7 @@ private extension HomeViewModel { self.state.journeyDidCancelled.send(deletedJourney) case .failure(let error): MSLogger.make(category: .home).error("\(error)") + self.state.journeyDidCancelled.send(completion: .failure(error)) } } } diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift index 36644e4..c4fa331 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift @@ -71,7 +71,7 @@ extension MapViewController { #endif } - public func recordingDidStop(_ stoppedJourney: RecordingJourney) { + public func recordingDidStop(_ stoppedJourney: RecordingJourney? = nil) { guard self.viewModel is RecordJourneyViewModel else { MSLogger.make(category: .home).error("여정이 종료되어야 하지만 이미 Map에서 NavigateMapViewModel을 사용하고 있습니다.") return @@ -85,7 +85,9 @@ extension MapViewController { self.locationManager.allowsBackgroundLocationUpdates = false #if DEBUG - MSLogger.make(category: .home).debug("여정 기록이 종료되었습니다: \(stoppedJourney)") + if let stoppedJourney { + MSLogger.make(category: .home).debug("여정 기록이 종료되었습니다: \(stoppedJourney)") + } #endif } diff --git a/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift b/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift index ffe90ef..fdfa658 100644 --- a/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift +++ b/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift @@ -150,18 +150,16 @@ public struct JourneyRepositoryImplementation: JourneyRepository { public mutating func deleteJourney(_ recordingJourney: RecordingJourney, userID: UUID) async -> Result { + defer { try? self.recordingJourney.finish() } + let requestDTO = DeleteJourneyRequestDTO(userID: userID, journeyID: recordingJourney.id) let router = JourneyRouter.deleteJourney(dto: requestDTO) let result = await self.networking.request(DeleteJourneyResponseDTO.self, router: router) + switch result { case .success(let responseDTO): - do { - try self.recordingJourney.finish() - let deletedJourney = responseDTO.toDomain() - return .success(deletedJourney) - } catch { - return .failure(error) - } + let deletedJourney = responseDTO.toDomain() + return .success(deletedJourney) case .failure(let error): return .failure(error) } From b34a101e447bf5180647702a9b4d72d407cc0293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Thu, 11 Jan 2024 01:08:01 +0900 Subject: [PATCH 17/20] =?UTF-8?q?:bug:=20=EC=97=AC=EA=B8=B0=EC=84=9C=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EA=B2=80=EC=83=89=20=EC=8B=9C=20=EC=9D=B4?= =?UTF-8?q?=EC=A0=84=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98?= =?UTF-8?q?=EC=9D=B4=20=EC=82=AD=EC=A0=9C=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Common/MapViewController.swift | 13 +++++++------ .../Common/View/Map/ClusterAnnotationView.swift | 8 +++++--- ...stomAnnotation.swift => SpotAnnotation.swift} | 4 ++-- ...tationView.swift => SpotAnnotationView.swift} | 16 +++++++--------- 4 files changed, 21 insertions(+), 20 deletions(-) rename iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/{CustomAnnotation.swift => SpotAnnotation.swift} (85%) rename iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/{CustomAnnotationView.swift => SpotAnnotationView.swift} (88%) diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift index a7bce5b..327ab4a 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift @@ -45,8 +45,8 @@ public final class MapViewController: UIViewController { mapView.userTrackingMode = .follow mapView.showsCompass = false - mapView.register(CustomAnnotationView.self, - forAnnotationViewWithReuseIdentifier: CustomAnnotationView.identifier) + mapView.register(SpotAnnotationView.self, + forAnnotationViewWithReuseIdentifier: SpotAnnotationView.identifier) mapView.register(ClusterAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier) @@ -153,6 +153,7 @@ public final class MapViewController: UIViewController { .receive(on: DispatchQueue.main) .sink { [weak self] journeys in self?.clearOverlays() + self?.clearAnnotations() self?.addAnnotations(with: journeys) self?.drawJourneyListPolylines(with: journeys) } @@ -180,9 +181,9 @@ public final class MapViewController: UIViewController { // MARK: - Functions: Annotation /// 식별자를 갖고 Annotation view 생성 - func addAnnotationView(using annotation: CustomAnnotation, + func addAnnotationView(using annotation: SpotAnnotation, on mapView: MKMapView) -> MKAnnotationView { - return mapView.dequeueReusableAnnotationView(withIdentifier: CustomAnnotationView.identifier, + return mapView.dequeueReusableAnnotationView(withIdentifier: SpotAnnotationView.identifier, for: annotation) } @@ -216,7 +217,7 @@ public final class MapViewController: UIViewController { private func addAnnotation(title: String, coordinate: CLLocationCoordinate2D, photoData: Data) { - let annotation = CustomAnnotation(title: title, + let annotation = SpotAnnotation(title: title, coordinate: coordinate, photoData: photoData) self.mapView.addAnnotation(annotation) @@ -385,7 +386,7 @@ extension MapViewController: MKMapViewDelegate { reuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier) } - let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: CustomAnnotationView.identifier, + let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: SpotAnnotationView.identifier, for: annotation) return annotationView } diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/ClusterAnnotationView.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/ClusterAnnotationView.swift index edadb5b..9ca2fca 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/ClusterAnnotationView.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/ClusterAnnotationView.swift @@ -7,14 +7,16 @@ import MapKit +import MSDesignSystem + final class ClusterAnnotationView: MKAnnotationView { // MARK: - Constants private enum Metric { - static let markerWidth: CGFloat = 43.0 - static let markerHeight: CGFloat = 53.0 + static let markerWidth: CGFloat = 60.0 + static let markerHeight: CGFloat = 60.0 static let inset: CGFloat = 4 static let thumbnailImageViewSize: CGFloat = Metric.markerWidth - Metric.inset * 2 @@ -62,7 +64,7 @@ final class ClusterAnnotationView: MKAnnotationView { override func layoutSubviews() { super.layoutSubviews() - bounds.size = self.markerImageView.bounds.size + self.bounds.size = self.markerImageView.bounds.size } // MARK: - UI Configuration diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/CustomAnnotation.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/SpotAnnotation.swift similarity index 85% rename from iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/CustomAnnotation.swift rename to iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/SpotAnnotation.swift index 46f9710..5247235 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/CustomAnnotation.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/SpotAnnotation.swift @@ -1,5 +1,5 @@ // -// CustomAnnotation.swift +// SpotAnnotation.swift // Home // // Created by 이창준 on 2023.12.06. @@ -8,7 +8,7 @@ import Foundation import MapKit -final class CustomAnnotation: NSObject, MKAnnotation { +final class SpotAnnotation: NSObject, MKAnnotation { // MARK: - Properties diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/CustomAnnotationView.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/SpotAnnotationView.swift similarity index 88% rename from iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/CustomAnnotationView.swift rename to iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/SpotAnnotationView.swift index 2a5a046..432bddb 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/CustomAnnotationView.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/View/Map/SpotAnnotationView.swift @@ -1,5 +1,5 @@ // -// CustomAnnotationView.swift +// SpotAnnotationView.swift // Home // // Created by 윤동주 on 11/23/23. @@ -11,16 +11,16 @@ import UIKit import MSDesignSystem -final class CustomAnnotationView: MKAnnotationView { +final class SpotAnnotationView: MKAnnotationView { // MARK: - Constants - public static let identifier = "CustomAnnotationView" + public static let identifier = "SpotAnnotationView" private enum Metric { - static let markerWidth: CGFloat = 43.0 - static let markerHeight: CGFloat = 53.0 + static let markerWidth: CGFloat = 60.0 + static let markerHeight: CGFloat = 60.0 static let inset: CGFloat = 4.0 static let thumbnailImageViewSize: CGFloat = Metric.markerWidth - Metric.inset * 2 @@ -42,9 +42,7 @@ final class CustomAnnotationView: MKAnnotationView { // MARK: - Properties override var annotation: MKAnnotation? { - didSet { - self.clusteringIdentifier = "spotIdentifier" - } + didSet { self.clusteringIdentifier = "spotIdentifier" } } // MARK: - Initializer @@ -103,7 +101,7 @@ final class CustomAnnotationView: MKAnnotationView { // MARK: - Functions func updateThumbnailImage() { - guard let annotation = self.annotation as? CustomAnnotation else { return } + guard let annotation = self.annotation as? SpotAnnotation else { return } self.thumbnailImageView.image = UIImage(data: annotation.photoData) } From 51306d13bcf9c33e208abe72ec4cb10956cb1644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Thu, 11 Jan 2024 10:14:31 +0900 Subject: [PATCH 18/20] =?UTF-8?q?:bug:=20=EC=97=AC=EC=A0=95=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=A4=91=20=EC=8A=A4=ED=8C=9F=20=ED=9B=84=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=EC=9D=B4=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=83=81=EC=97=90=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Presentation/HomeViewController.swift | 10 +++++++--- .../RecordJourneyButtonStackView.swift | 4 ++-- .../MapViewController+EventListener.swift | 15 +++++++++++++++ .../Common/MapViewController.swift | 8 ++------ .../Coordinator/SpotNavigationDelegate.swift | 6 +++--- .../Presentation/SaveSpotViewController.swift | 4 ++-- .../Spot/Presentation/SaveSpotViewModel.swift | 4 ++-- .../MusicSpot.xcodeproj/project.pbxproj | 4 ++++ .../Coordinator/HomeCoordinator.swift | 18 ++++++++++++++++++ .../SpotCoordinatorFinishDelegate.swift | 16 ++++++++++++++++ .../Coordinator/SpotCoordinator.swift | 8 +++++--- 11 files changed, 76 insertions(+), 21 deletions(-) create mode 100644 iOS/MusicSpot/MusicSpot/Coordinator/Protocol/SpotCoordinatorFinishDelegate.swift diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift index 1bf3e11..31851ca 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift @@ -209,6 +209,10 @@ public final class HomeViewController: HomeBottomSheetViewController { }) } + public func spotDidAdded(_ spot: Spot, photoData: Data) { + self.contentViewController.spotDidAdded(spot, photoData: photoData) + } + } // MARK: - Buttons @@ -240,13 +244,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) } - 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 { @@ -256,7 +260,7 @@ 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 { diff --git a/iOS/Features/Home/Sources/Home/Presentation/RecordJourneyButtonStackView.swift b/iOS/Features/Home/Sources/Home/Presentation/RecordJourneyButtonStackView.swift index 765418f..bb084da 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/RecordJourneyButtonStackView.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/RecordJourneyButtonStackView.swift @@ -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) @@ -64,7 +64,7 @@ public final class RecordJourneyButtonStackView: UIView { // MARK: - Properties - public var delegate: RecordJourneyButtonViewDelegate? + internal var delegate: RecordJourneyButtonViewDelegate? // MARK: - Initializer diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift index c4fa331..b2b19e8 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift @@ -92,3 +92,18 @@ extension MapViewController { } } + +// 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) + } + +} diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift index 327ab4a..5791986 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift @@ -214,12 +214,8 @@ public final class MapViewController: UIViewController { } } - private func addAnnotation(title: String, - coordinate: CLLocationCoordinate2D, - photoData: Data) { - let annotation = SpotAnnotation(title: title, - coordinate: coordinate, - photoData: photoData) + func addAnnotation(title: String, coordinate: CLLocationCoordinate2D, photoData: Data) { + let annotation = SpotAnnotation(title: title, coordinate: coordinate, photoData: photoData) self.mapView.addAnnotation(annotation) } diff --git a/iOS/Features/Spot/Sources/Spot/Presentation/Coordinator/SpotNavigationDelegate.swift b/iOS/Features/Spot/Sources/Spot/Presentation/Coordinator/SpotNavigationDelegate.swift index d84f269..82b568b 100644 --- a/iOS/Features/Spot/Sources/Spot/Presentation/Coordinator/SpotNavigationDelegate.swift +++ b/iOS/Features/Spot/Sources/Spot/Presentation/Coordinator/SpotNavigationDelegate.swift @@ -14,14 +14,14 @@ public protocol SpotNavigationDelegate: AnyObject { func presentPhotoLibrary(from viewController: UIViewController) func presentSaveSpot(using image: UIImage, coordinate: Coordinate) func dismissToSpot() - func popToHome(spot: Spot?) + func popToHome(with spot: Spot?, photoData: Data?) } extension SpotNavigationDelegate { - public func popToHome(spot: Spot? = nil) { - self.popToHome(spot: nil) + public func popToHome(with spot: Spot? = nil, photoData: Data? = nil) { + self.popToHome(with: nil, photoData: nil) } } diff --git a/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewController.swift b/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewController.swift index f6a882e..6ef1d47 100644 --- a/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewController.swift +++ b/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewController.swift @@ -116,8 +116,8 @@ public final class SaveSpotViewController: UIViewController { private func bind() { self.viewModel.state.uploadedSpot .receive(on: DispatchQueue.main) - .sink { [weak self] spot in - self?.navigationDelegate?.popToHome(spot: spot) + .sink { [weak self] spot, photoData in + self?.navigationDelegate?.popToHome(with: spot, photoData: photoData) } .store(in: &self.cancellables) } diff --git a/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewModel.swift b/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewModel.swift index 6db1c41..73c534b 100644 --- a/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewModel.swift +++ b/iOS/Features/Spot/Sources/Spot/Presentation/SaveSpotViewModel.swift @@ -20,7 +20,7 @@ public final class SaveSpotViewModel { public struct State { // Passthrough - public var uploadedSpot = PassthroughSubject() + public var uploadedSpot = PassthroughSubject<(Spot, Data), Never>() } // MARK: - Properties @@ -66,7 +66,7 @@ internal extension SaveSpotViewModel { let result = await self.spotRepository.upload(spot: spot) switch result { case .success(let spot): - self.state.uploadedSpot.send(spot) + self.state.uploadedSpot.send((spot, data)) MSLogger.make(category: .network).debug("성공적으로 업로드되었습니다: \(spot)") case .failure(let error): MSLogger.make(category: .network).error("\(error): 업로드에 실패하였습니다.") diff --git a/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj b/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj index 7daa0d7..e81d46b 100644 --- a/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj +++ b/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ DD92FEB52B35EE0700A672F5 /* Spot in Frameworks */ = {isa = PBXBuildFile; productRef = DD92FEB42B35EE0700A672F5 /* Spot */; }; DD92FEB82B35EE5500A672F5 /* MSUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = DD92FEB72B35EE5500A672F5 /* MSUIKit */; }; DD92FEBA2B35EEAF00A672F5 /* SaveJourney in Frameworks */ = {isa = PBXBuildFile; productRef = DD92FEB92B35EEAF00A672F5 /* SaveJourney */; }; + DDB3B9F22B4F6DF60024C6BF /* SpotCoordinatorFinishDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB3B9F12B4F6DF60024C6BF /* SpotCoordinatorFinishDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -58,6 +59,7 @@ DD73F8662B024C4B00EE9BF2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DD73F86D2B024CA000EE9BF2 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/LaunchScreen.strings; sourceTree = ""; }; DDB22F742B433EC300F83D84 /* CoordinatorFinishDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorFinishDelegate.swift; sourceTree = ""; }; + DDB3B9F12B4F6DF60024C6BF /* SpotCoordinatorFinishDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotCoordinatorFinishDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -97,6 +99,7 @@ children = ( 08CBF8772B18468E007D3797 /* Coordinator.swift */, DDB22F742B433EC300F83D84 /* CoordinatorFinishDelegate.swift */, + DDB3B9F12B4F6DF60024C6BF /* SpotCoordinatorFinishDelegate.swift */, ); path = Protocol; sourceTree = ""; @@ -227,6 +230,7 @@ 08CBF87F2B18468E007D3797 /* Coordinator.swift in Sources */, 08CBF87D2B18468E007D3797 /* SpotCoordinator.swift in Sources */, 08CBF87E2B18468E007D3797 /* SaveJourneyFlowCoordinator.swift in Sources */, + DDB3B9F22B4F6DF60024C6BF /* SpotCoordinatorFinishDelegate.swift in Sources */, 08CBF87A2B18468E007D3797 /* RewindJourneyCoordinator.swift in Sources */, DD73F8592B024C4900EE9BF2 /* AppDelegate.swift in Sources */, 08CBF87B2B18468E007D3797 /* HomeCoordinator.swift in Sources */, diff --git a/iOS/MusicSpot/MusicSpot/Coordinator/HomeCoordinator.swift b/iOS/MusicSpot/MusicSpot/Coordinator/HomeCoordinator.swift index bc68236..db854d0 100644 --- a/iOS/MusicSpot/MusicSpot/Coordinator/HomeCoordinator.swift +++ b/iOS/MusicSpot/MusicSpot/Coordinator/HomeCoordinator.swift @@ -77,6 +77,23 @@ extension HomeCoordinator: CoordinatorFinishDelegate { } +// MARK: - Finish Delegate: Spot + +extension HomeCoordinator: SpotCoordinatorFinishDelegate { + + func shouldFinish(childCoordinator: Coordinator, with spot: Spot?, photoData: Data?) { + guard let homeViewController = self.rootViewController as? HomeViewController else { return } + + if let spot = spot, let photoData = photoData { + homeViewController.spotDidAdded(spot, photoData: photoData) + } + + self.shouldFinish(childCoordinator: childCoordinator) + } + +} + + // MARK: - Home Navigation extension HomeCoordinator: HomeNavigationDelegate { @@ -84,6 +101,7 @@ extension HomeCoordinator: HomeNavigationDelegate { func navigateToSpot(spotCoordinate coordinate: Coordinate) { let spotCoordinator = SpotCoordinator(navigationController: self.navigationController) spotCoordinator.finishDelegate = self + spotCoordinator.spotFinishDelegate = self self.childCoordinators.append(spotCoordinator) spotCoordinator.start(spotCoordinate: coordinate) } diff --git a/iOS/MusicSpot/MusicSpot/Coordinator/Protocol/SpotCoordinatorFinishDelegate.swift b/iOS/MusicSpot/MusicSpot/Coordinator/Protocol/SpotCoordinatorFinishDelegate.swift new file mode 100644 index 0000000..73d2b70 --- /dev/null +++ b/iOS/MusicSpot/MusicSpot/Coordinator/Protocol/SpotCoordinatorFinishDelegate.swift @@ -0,0 +1,16 @@ +// +// SpotCoordinatorFinishDelegate.swift +// MusicSpot +// +// Created by 이창준 on 2024.01.11. +// + +import Foundation + +import MSDomain + +protocol SpotCoordinatorFinishDelegate: AnyObject { + + func shouldFinish(childCoordinator: Coordinator, with spot: Spot?, photoData: Data?) + +} diff --git a/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift b/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift index 84eb1fa..41b400d 100644 --- a/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift +++ b/iOS/MusicSpot/MusicSpot/Coordinator/SpotCoordinator.swift @@ -20,6 +20,7 @@ final class SpotCoordinator: Coordinator { var childCoordinators: [Coordinator] = [] weak var finishDelegate: CoordinatorFinishDelegate? + weak var spotFinishDelegate: SpotCoordinatorFinishDelegate? // MARK: - Initializer @@ -82,13 +83,14 @@ extension SpotCoordinator: SpotNavigationDelegate { spotSaveViewController.dismiss(animated: true) } - func popToHome(spot: Spot?) { + func popToHome(with spot: Spot?, photoData: Data?) { if let presentedViewController = self.navigationController.presentedViewController { presentedViewController.dismiss(animated: true) { [weak self] in - self?.finish() + guard let self = self else { return } + self.spotFinishDelegate?.shouldFinish(childCoordinator: self, with: spot, photoData: photoData) } } else { - self.finish() + self.spotFinishDelegate?.shouldFinish(childCoordinator: self, with: spot, photoData: photoData) } } From 0a574d0fce5e7a538b24291d2adb76f53ecbd990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Thu, 11 Jan 2024 10:30:10 +0900 Subject: [PATCH 19/20] =?UTF-8?q?:sparkles:=20=EC=97=AC=EC=A0=95=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=8B=9C=20=EB=B2=84=ED=8A=BC=EC=97=90=20?= =?UTF-8?q?=EB=A1=9C=EB=94=A9=20=EC=9D=B8=EB=94=94=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/SaveJourneyViewController.swift | 10 ++++++++++ .../Presentation/SaveJourneyViewModel.swift | 10 +++++++--- .../Sources/MSUIKit/MSAlertViewController.swift | 4 ++++ .../MusicSpot/Coordinator/HomeCoordinator.swift | 1 - 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewController.swift b/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewController.swift index b5298a1..6eb192d 100644 --- a/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewController.swift +++ b/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewController.swift @@ -106,6 +106,8 @@ public final class SaveJourneyViewController: UIViewController { return button }() + private var alertView: MSAlertViewController? + // MARK: - Initializer public init(viewModel: SaveJourneyViewModel, @@ -183,6 +185,14 @@ public final class SaveJourneyViewController: UIViewController { } .store(in: &self.cancellables) + self.viewModel.state.isDoneButtonLoading + .receive(on: DispatchQueue.main) + .sink { [weak self] isDoneButtonLoading in + guard let alertView = self?.alertView else { return } + alertView.updateDoneButtonLoadingState(to: isDoneButtonLoading) + } + .store(in: &self.cancellables) + self.viewModel.state.recordingJourney .compactMap { $0 } .receive(on: DispatchQueue.main) diff --git a/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewModel.swift b/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewModel.swift index ccb308f..c6d9063 100644 --- a/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewModel.swift +++ b/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewModel.swift @@ -28,6 +28,7 @@ public final class SaveJourneyViewModel { /// Apple Music 권한 상태 var musicAuthorizatonStatus = CurrentValueSubject(.notDetermined) var buttonStateFactors = CurrentValueSubject(ButtonStateFactor()) + var isDoneButtonLoading = CurrentValueSubject(false) var recordingJourney = CurrentValueSubject(nil) var selectedSong: CurrentValueSubject @@ -93,19 +94,22 @@ public final class SaveJourneyViewModel { private extension SaveJourneyViewModel { func endJourney(named title: String) { - guard let recordingJourney = self.state.recordingJourney.value, - let journeyID = self.journeyRepository.recordingJourneyID else { + self.state.isDoneButtonLoading.send(true) + defer { self.state.isDoneButtonLoading.send(false) } + + guard let recordingJourney = self.state.recordingJourney.value else { return } let selectedSong = self.state.selectedSong.value let coordinates = recordingJourney.coordinates + [self.lastCoordiante] - let journey = Journey(id: journeyID, + let journey = Journey(id: recordingJourney.id, title: title, date: (start: recordingJourney.startTimestamp, end: .now), spots: recordingJourney.spots, coordinates: coordinates, music: Music(selectedSong)) + Task { let result = await self.journeyRepository.endJourney(journey) switch result { diff --git a/iOS/MSUIKit/Sources/MSUIKit/MSAlertViewController.swift b/iOS/MSUIKit/Sources/MSUIKit/MSAlertViewController.swift index d41862f..71750e1 100644 --- a/iOS/MSUIKit/Sources/MSUIKit/MSAlertViewController.swift +++ b/iOS/MSUIKit/Sources/MSUIKit/MSAlertViewController.swift @@ -319,4 +319,8 @@ open class MSAlertViewController: UIViewController { self.doneButton.isEnabled = isEnabled } + public func updateDoneButtonLoadingState(to isLoading: Bool) { + self.doneButton.configuration?.showsActivityIndicator = isLoading + } + } diff --git a/iOS/MusicSpot/MusicSpot/Coordinator/HomeCoordinator.swift b/iOS/MusicSpot/MusicSpot/Coordinator/HomeCoordinator.swift index db854d0..a28de6e 100644 --- a/iOS/MusicSpot/MusicSpot/Coordinator/HomeCoordinator.swift +++ b/iOS/MusicSpot/MusicSpot/Coordinator/HomeCoordinator.swift @@ -93,7 +93,6 @@ extension HomeCoordinator: SpotCoordinatorFinishDelegate { } - // MARK: - Home Navigation extension HomeCoordinator: HomeNavigationDelegate { From db2c736586a97f2d1751215ffc5f8d92f78d4415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Thu, 11 Jan 2024 20:48:13 +0900 Subject: [PATCH 20/20] =?UTF-8?q?:bug:=20=EC=97=AC=EC=A0=95=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=ED=9B=84=EC=97=90=EB=8F=84=20=EC=97=AC=EC=A0=95?= =?UTF-8?q?=EC=9D=B4=20=EA=B8=B0=EB=A1=9D=EB=90=98=EB=8A=94=20=ED=98=84?= =?UTF-8?q?=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Coordinator/HomeNavigationDelegate.swift | 2 +- .../Presentation/HomeViewController.swift | 12 ++++++++- .../Home/Presentation/HomeViewModel.swift | 7 ++++++ .../SaveJourneyNavigationDelegate.swift | 2 +- .../SaveJourneyViewController.swift | 4 +-- .../JourneyRepository.swift | 2 +- .../Storage/RecordingJourneyStorage.swift | 4 +++ .../MSDomain/Model/RecordingJourney.swift | 7 ++++++ .../MusicSpot.xcodeproj/project.pbxproj | 4 +++ .../Coordinator/HomeCoordinator.swift | 25 +++++++++++++++---- ...JourneyFlowCoordinatorFinishDelegate.swift | 16 ++++++++++++ .../SaveJourneyFlowCoordinator.swift | 5 ++++ 12 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 iOS/MusicSpot/MusicSpot/Coordinator/Protocol/SaveJourneyFlowCoordinatorFinishDelegate.swift diff --git a/iOS/Features/Home/Sources/Home/Presentation/Coordinator/HomeNavigationDelegate.swift b/iOS/Features/Home/Sources/Home/Presentation/Coordinator/HomeNavigationDelegate.swift index 21b137b..3a7b3ed 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/Coordinator/HomeNavigationDelegate.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/Coordinator/HomeNavigationDelegate.swift @@ -12,6 +12,6 @@ import MSDomain public protocol HomeNavigationDelegate: AnyObject { func navigateToSpot(spotCoordinate coordinate: Coordinate) - func navigateToSelectSong(lastCoordinate: Coordinate) + func navigateToSaveJourneyFlow(lastCoordinate: Coordinate) } diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift index 31851ca..a337e02 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift @@ -116,6 +116,12 @@ public final class HomeViewController: HomeBottomSheetViewController { self.navigationController?.isNavigationBarHidden = true } + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.viewModel.trigger(.viewNeedsReloaded) + } + public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -213,6 +219,10 @@ public final class HomeViewController: HomeBottomSheetViewController { self.contentViewController.spotDidAdded(spot, photoData: photoData) } + public func journeyDidEnded(endedJourney: Journey) { + self.contentViewController.recordingDidStop(RecordingJourney(endedJourney)) + } + } // MARK: - Buttons @@ -267,7 +277,7 @@ extension HomeViewController: RecordJourneyButtonViewDelegate { return } - self.navigationDelegate?.navigateToSelectSong(lastCoordinate: currentUserCoordiante) + self.navigationDelegate?.navigateToSaveJourneyFlow(lastCoordinate: currentUserCoordiante) } } diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift index 052cf92..dec9b83 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift @@ -20,6 +20,7 @@ public final class HomeViewModel { public enum Action { case viewNeedsLoaded + case viewNeedsReloaded case startButtonDidTap(Coordinate) case refreshButtonDidTap(visibleCoordinates: (minCoordinate: Coordinate, maxCoordinate: Coordinate)) case backButtonDidTap @@ -71,6 +72,8 @@ public final class HomeViewModel { self.createNewUserWhenFirstLaunch() self.resumeJourneyIfNeeded() + case .viewNeedsReloaded: + self.syncRecordingState() case .startButtonDidTap(let coordinate): #if DEBUG MSLogger.make(category: .home).debug("시작 버튼이 탭 되었습니다: \(coordinate)") @@ -188,6 +191,10 @@ private extension HomeViewModel { func syncRecordingState() { let isRecording = self.journeyRepository.isRecording + #if DEBUG + MSLogger.make(category: .home) + .debug("여정 기록 여부를 싱크하고 있습니다. 현재 기록 상태: \(isRecording ? "기록 중" : "기록 중이지 않음")") + #endif self.state.isRecording.send(isRecording) } diff --git a/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/Coordinator/SaveJourneyNavigationDelegate.swift b/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/Coordinator/SaveJourneyNavigationDelegate.swift index eef19ad..6d98b2e 100644 --- a/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/Coordinator/SaveJourneyNavigationDelegate.swift +++ b/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/Coordinator/SaveJourneyNavigationDelegate.swift @@ -11,7 +11,7 @@ import MSDomain public protocol SaveJourneyNavigationDelegate: AnyObject { - func popToHome() + func popToHome(with endedJourney: Journey) func popToSelectSong() } diff --git a/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewController.swift b/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewController.swift index 6eb192d..3a4b719 100644 --- a/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewController.swift +++ b/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewController.swift @@ -207,8 +207,8 @@ public final class SaveJourneyViewController: UIViewController { self.viewModel.state.endJourneySucceed .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.navigationDelegate?.popToHome() + .sink { [weak self] endedJourney in + self?.navigationDelegate?.popToHome(with: endedJourney) } .store(in: &self.cancellables) } diff --git a/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift b/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift index fdfa658..59b1fe4 100644 --- a/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift +++ b/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift @@ -117,7 +117,7 @@ public struct JourneyRepositoryImplementation: JourneyRepository { case .success(let responseDTO): let coordinates = responseDTO.coordinates.map { $0.toDomain() } let recordingJourney = RecordingJourney(id: journeyID, - startTimestamp: Date(), + startTimestamp: Date.now, spots: [], coordinates: coordinates) self.recordingJourney.record(responseDTO.coordinates, keyPath: \.coordinates) diff --git a/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift b/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift index 456e1ac..74d233d 100644 --- a/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift +++ b/iOS/MSData/Sources/MSData/Storage/RecordingJourneyStorage.swift @@ -102,6 +102,10 @@ public struct RecordingJourneyStorage { self.isRecording = false self.recordingJourneyID = nil + + #if DEBUG + MSLogger.make(category: .recordingJourneyStorage).debug("여정 기록을 종료합니다: \(recordingJourneyID)") + #endif } } diff --git a/iOS/MSDomain/Sources/MSDomain/Model/RecordingJourney.swift b/iOS/MSDomain/Sources/MSDomain/Model/RecordingJourney.swift index df35762..d326f77 100644 --- a/iOS/MSDomain/Sources/MSDomain/Model/RecordingJourney.swift +++ b/iOS/MSDomain/Sources/MSDomain/Model/RecordingJourney.swift @@ -29,6 +29,13 @@ public struct RecordingJourney: Identifiable { self.coordinates = coordinates } + public init(_ journey: Journey) { + self.id = journey.id + self.startTimestamp = journey.date.start + self.spots = journey.spots + self.coordinates = journey.coordinates + } + // MARK: - Subscript subscript(dynamicMember keyPath: KeyPath) -> T { diff --git a/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj b/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj index e81d46b..0bdc1d0 100644 --- a/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj +++ b/iOS/MusicSpot/MusicSpot.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 08CBF87D2B18468E007D3797 /* SpotCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBF8752B18468E007D3797 /* SpotCoordinator.swift */; }; 08CBF87E2B18468E007D3797 /* SaveJourneyFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBF8762B18468E007D3797 /* SaveJourneyFlowCoordinator.swift */; }; 08CBF87F2B18468E007D3797 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBF8772B18468E007D3797 /* Coordinator.swift */; }; + DD612FB92B50080600E681CC /* SaveJourneyFlowCoordinatorFinishDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD612FB82B50080600E681CC /* SaveJourneyFlowCoordinatorFinishDelegate.swift */; }; DD72AF642B4B89E700EB4E81 /* CoordinatorFinishDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB22F742B433EC300F83D84 /* CoordinatorFinishDelegate.swift */; }; DD73F8592B024C4900EE9BF2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73F8582B024C4900EE9BF2 /* AppDelegate.swift */; }; DD73F85B2B024C4900EE9BF2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73F85A2B024C4900EE9BF2 /* SceneDelegate.swift */; }; @@ -49,6 +50,7 @@ 08CBF8752B18468E007D3797 /* SpotCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpotCoordinator.swift; sourceTree = ""; }; 08CBF8762B18468E007D3797 /* SaveJourneyFlowCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveJourneyFlowCoordinator.swift; sourceTree = ""; }; 08CBF8772B18468E007D3797 /* Coordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; + DD612FB82B50080600E681CC /* SaveJourneyFlowCoordinatorFinishDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveJourneyFlowCoordinatorFinishDelegate.swift; sourceTree = ""; }; DD72AF192B4A4A8800EB4E81 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; DD72AF1B2B4A4A8800EB4E81 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; DD73F8552B024C4900EE9BF2 /* MusicSpot.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MusicSpot.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -100,6 +102,7 @@ 08CBF8772B18468E007D3797 /* Coordinator.swift */, DDB22F742B433EC300F83D84 /* CoordinatorFinishDelegate.swift */, DDB3B9F12B4F6DF60024C6BF /* SpotCoordinatorFinishDelegate.swift */, + DD612FB82B50080600E681CC /* SaveJourneyFlowCoordinatorFinishDelegate.swift */, ); path = Protocol; sourceTree = ""; @@ -227,6 +230,7 @@ buildActionMask = 2147483647; files = ( 08CBF8792B18468E007D3797 /* AppCoordinator.swift in Sources */, + DD612FB92B50080600E681CC /* SaveJourneyFlowCoordinatorFinishDelegate.swift in Sources */, 08CBF87F2B18468E007D3797 /* Coordinator.swift in Sources */, 08CBF87D2B18468E007D3797 /* SpotCoordinator.swift in Sources */, 08CBF87E2B18468E007D3797 /* SaveJourneyFlowCoordinator.swift in Sources */, diff --git a/iOS/MusicSpot/MusicSpot/Coordinator/HomeCoordinator.swift b/iOS/MusicSpot/MusicSpot/Coordinator/HomeCoordinator.swift index a28de6e..a2a31e4 100644 --- a/iOS/MusicSpot/MusicSpot/Coordinator/HomeCoordinator.swift +++ b/iOS/MusicSpot/MusicSpot/Coordinator/HomeCoordinator.swift @@ -93,6 +93,20 @@ extension HomeCoordinator: SpotCoordinatorFinishDelegate { } +// MARK: - Finish Delegate: SaveJourneyFlow + +extension HomeCoordinator: SaveJourneyFlowCoordinatorFinishDelegate { + + func shouldFinish(childCoordinator: Coordinator, with endedJourney: Journey) { + guard let homeViewController = self.rootViewController as? HomeViewController else { return } + + homeViewController.journeyDidEnded(endedJourney: endedJourney) + + self.shouldFinish(childCoordinator: childCoordinator) + } + +} + // MARK: - Home Navigation extension HomeCoordinator: HomeNavigationDelegate { @@ -105,11 +119,12 @@ extension HomeCoordinator: HomeNavigationDelegate { spotCoordinator.start(spotCoordinate: coordinate) } - func navigateToSelectSong(lastCoordinate: Coordinate) { - let selectSongCoordinator = SaveJourneyFlowCoordinator(navigationController: self.navigationController) - selectSongCoordinator.finishDelegate = self - self.childCoordinators.append(selectSongCoordinator) - selectSongCoordinator.start(lastCoordinate: lastCoordinate) + func navigateToSaveJourneyFlow(lastCoordinate: Coordinate) { + let saveJourneyFlowCoordinator = SaveJourneyFlowCoordinator(navigationController: self.navigationController) + saveJourneyFlowCoordinator.finishDelegate = self + saveJourneyFlowCoordinator.saveJourneyFinishDelegate = self + self.childCoordinators.append(saveJourneyFlowCoordinator) + saveJourneyFlowCoordinator.start(lastCoordinate: lastCoordinate) } } diff --git a/iOS/MusicSpot/MusicSpot/Coordinator/Protocol/SaveJourneyFlowCoordinatorFinishDelegate.swift b/iOS/MusicSpot/MusicSpot/Coordinator/Protocol/SaveJourneyFlowCoordinatorFinishDelegate.swift new file mode 100644 index 0000000..ef0a6aa --- /dev/null +++ b/iOS/MusicSpot/MusicSpot/Coordinator/Protocol/SaveJourneyFlowCoordinatorFinishDelegate.swift @@ -0,0 +1,16 @@ +// +// SaveJourneyFlowCoordinatorFinishDelegate.swift +// MusicSpot +// +// Created by 이창준 on 2024.01.11. +// + +import Foundation + +import MSDomain + +protocol SaveJourneyFlowCoordinatorFinishDelegate: AnyObject { + + func shouldFinish(childCoordinator: Coordinator, with endedJourney: Journey) + +} diff --git a/iOS/MusicSpot/MusicSpot/Coordinator/SaveJourneyFlowCoordinator.swift b/iOS/MusicSpot/MusicSpot/Coordinator/SaveJourneyFlowCoordinator.swift index adef178..371688a 100644 --- a/iOS/MusicSpot/MusicSpot/Coordinator/SaveJourneyFlowCoordinator.swift +++ b/iOS/MusicSpot/MusicSpot/Coordinator/SaveJourneyFlowCoordinator.swift @@ -22,6 +22,7 @@ final class SaveJourneyFlowCoordinator: Coordinator { var childCoordinators: [Coordinator] = [] weak var finishDelegate: CoordinatorFinishDelegate? + weak var saveJourneyFinishDelegate: SaveJourneyFlowCoordinatorFinishDelegate? // MARK: - Initializer @@ -84,6 +85,10 @@ extension SaveJourneyFlowCoordinator: SelectSongNavigationDelegate { extension SaveJourneyFlowCoordinator: SaveJourneyNavigationDelegate { + func popToHome(with endedJourney: Journey) { + self.saveJourneyFinishDelegate?.shouldFinish(childCoordinator: self, with: endedJourney) + } + func popToSelectSong() { guard self.navigationController.topViewController is SaveJourneyViewController else { return }