Skip to content

Commit

Permalink
Merge pull request #278 from boostcampwm2023/iOS/task/upgradeLocalSto…
Browse files Browse the repository at this point in the history
…rage

[iOS] Local Storage coordinate 외 정보들도 저장할 수 있게끔 수정
  • Loading branch information
SwiftyJunnos authored Dec 10, 2023
2 parents 378314d + 0d6e209 commit 01743cc
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ public final class FileManagerStorage: NSObject, MSPersistentStorage {
dateFormatter.formatOptions = [.withFractionalSeconds, .withTimeZone, .withInternetDateTime]
encoder.dateEncodingStrategy = .custom({ date, encoder in
var container = encoder.singleValueContainer()
try container.encode(date)
let dateString = dateFormatter.string(from: date)
try container.encode(dateString)
})
return encoder
}()

private let decoder: JSONDecoder = {
let decoder = JSONDecoder()
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withFractionalSeconds, .withTimeZone, .withInternetDateTime]
dateFormatter.formatOptions.insert(.withFractionalSeconds)
decoder.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
Expand All @@ -41,6 +42,7 @@ public final class FileManagerStorage: NSObject, MSPersistentStorage {
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Date 디코딩 실패: \(dateString)")
})
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()

Expand Down Expand Up @@ -74,9 +76,20 @@ public final class FileManagerStorage: NSObject, MSPersistentStorage {
if let path = self.storageURL()?.path,
let contents = try? self.fileManager.contentsOfDirectory(atPath: path) {
let allDecodedData: [T] = contents.compactMap { content in
guard let dataPath = URL(string: (path as NSString).appendingPathComponent(content)),
let data = try? Data(contentsOf: dataPath),
let decodedData = try? self.decoder.decode(T.self, from: data) else { return nil }
let key = String(content.dropLast(".json".count))
guard let dataPath =
fileURL(forKey: key) else {
MSLogger.make(category: .fileManager).error("경로의 Data를 가져오지 못하였습니다.")
return nil
}
MSLogger.make(category: .fileManager).error("경로의 Data를 성공적으로 가져왔습니다.")

print(dataPath.description)
guard let data = try? Data(contentsOf: dataPath),
let decodedData = try? self.decoder.decode(T.self, from: data) else {
MSLogger.make(category: .fileManager).error("decode에 실패하였습니다.")
return nil
}
return decodedData
}
return allDecodedData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,54 @@ final class MSPersistentStorageTests: XCTestCase {
}

func test_FileManagerStorage에서_모든데이터저장불러오기_성공() {
let sut = MockCodableData(title: "boostcamp", content: "wm8")
let key = "S045"
self.fileStorage.set(value: sut, forKey: key)
self.fileStorage.set(value: sut, forKey: key)
self.fileStorage.set(value: sut, forKey: key)
let sut1 = MockCodableData(title: "boostcamp", content: "wm8")
let sut2 = MockCodableData(title: "boostcamp", content: "wm8")
let key1 = "S045"
let key2 = "S034"

self.fileStorage.set(value: sut1, forKey: key1)
self.fileStorage.set(value: sut2, forKey: key2)

guard let allStoredData = self.fileStorage.getAllOf(MockCodableData.self) else {
XCTFail("데이터 읽기에 실패했습니다.")
return
}

XCTAssertEqual(allStoredData.count, 2, "데이터 저장에 실패하였습니다.")
XCTAssertTrue(allStoredData.allSatisfy { $0 == sut1 || $0 == sut2 })
}

func test_FileManagerStorage에서_모든데이터저장불러올때_폴더하위항목까지_읽을수있는지_실패() {
let sut1 = MockCodableData(title: "boostcamp", content: "wm8")
let sut2 = MockCodableData(title: "boostcamp", content: "wm8")
let key1 = "S045"
let key2 = "/handsome/jeonmingun/S034"

self.fileStorage.set(value: sut1, forKey: key1)
self.fileStorage.set(value: sut2, forKey: key2)

guard let allStoredData = self.fileStorage.getAllOf(MockCodableData.self) else {
XCTFail("데이터 읽기에 실패했습니다.")
return
}

XCTAssertEqual(allStoredData.count, 2, "데이터 저장에 실패하였습니다.")
XCTAssertFalse(allStoredData.allSatisfy { $0 == sut1 || $0 == sut2 })
}

func test_Date형식_저장할_수_있는지_성공() {
let sut = Date.now
let key = "S034"

allStoredData.forEach { storedData in
XCTAssertEqual(sut, storedData,
"목표 데이터와 불러온 값이 다릅니다.")
self.fileStorage.set(value: sut, forKey: key)

guard let storedData = self.fileStorage.get(Date.self, forKey: key) else {
XCTFail("데이터 읽기에 실패했습니다.")
return
}

XCTAssertEqual(sut.description, storedData.description,
"목표 데이터와 불러온 값이 다릅니다.")
}

}
10 changes: 10 additions & 0 deletions iOS/MSData/.swiftpm/xcode/xcshareddata/xcschemes/MSData.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "RepositoryTests"
BuildableName = "RepositoryTests"
BlueprintName = "RepositoryTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//
// JourneyRepository+Persistable.swift
// MSData
//
// Created by 전민건 on 12/10/23.
//

import Foundation

import MSDomain
import MSLogger
import MSPersistentStorage

public protocol Persistable {

func saveToLocal(value: Codable) -> Bool
func loadJourneyFromLocal() -> RecordingJourney?

}

// MARK: - Interface

extension JourneyRepositoryImplementation: Persistable {

private struct KeyStorage {

static var id: String? = nil
static var startTimestamp: String? = nil
static var spots = [String]()
static var coordinates = [String]()

}

@discardableResult
public func saveToLocal(value: Codable) -> Bool {
let key = UUID().uuidString
self.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 tamp는 하나의 값만 저장할 수 있습니다.")
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 loadJourneyFromLocal() -> RecordingJourney? {
guard let id = self.loadID(),
let startTimestamp = self.loadStartTimeStamp() else {
return nil
}
return RecordingJourney(id: id,
startTimestamp: startTimestamp,
spots: self.loadSpots(),
coordinates: self.loadCoordinates())
}

}

// MARK: - load Functions

private extension JourneyRepositoryImplementation {

func loadStartTimeStamp() -> Date? {
guard let startTimestampKey = KeyStorage.startTimestamp,
let startTimestamp = self.storage.get(Date.self, forKey: startTimestampKey)
else {
MSLogger.make(category: .persistable).debug("id 또는 startTimestamp가 저장되지 않았습니다.")
return nil
}
return startTimestamp
}

func loadID() -> String? {
guard let idKey = KeyStorage.id,
let id = self.storage.get(String.self, forKey: idKey) else {
MSLogger.make(category: .persistable).debug("id 또는 startTimestamp가 저장되지 않았습니다.")
return nil
}
return id
}

func loadSpots() -> [Spot] {
return KeyStorage.spots.compactMap { spotKey in
let spotDTO = self.storage.get(SpotDTO.self, forKey: spotKey)
return spotDTO?.toDomain()
}
}

func loadCoordinates() -> [Coordinate] {
return KeyStorage.coordinates.compactMap { coordinateKey in
let coordinateDTO = self.storage.get(CoordinateDTO.self, forKey: coordinateKey)
return coordinateDTO?.toDomain()
}
}

}
4 changes: 4 additions & 0 deletions iOS/MSData/Sources/MSData/Repository/JourneyRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ public struct JourneyRepositoryImplementation: JourneyRepository {
@UserDefaultsWrapped(UserDefaultsKey.recordingJourneyID, defaultValue: nil)
private var recordingJourneyID: String?

// MARK: - Properties: Persistable

internal var storage = FileManagerStorage()

// MARK: - Initializer

public init(session: URLSession = URLSession(configuration: .default),
Expand Down
50 changes: 0 additions & 50 deletions iOS/MSData/Sources/MSData/Repository/LocalRepository.swift

This file was deleted.

57 changes: 57 additions & 0 deletions iOS/MSData/Tests/RepositoryTests/PersistableRepositoryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// PersistableRepositoryTests.swift
//
//
// Created by 전민건 on 12/11/23.
//

import XCTest
@testable import MSData
@testable import MSDomain

final class PersistableRepositoryTests: XCTestCase {

// MARK: - Properties

private let journeyRepository = JourneyRepositoryImplementation()

// MARK: - Tests

func test_Spot저장_성공() {
let coordinate = Coordinate(latitude: 10, longitude: 10)
let url = URL(string: "/../")!

let spot = Spot(coordinate: coordinate, timestamp: .now, photoURL: url)

XCTAssertTrue(self.journeyRepository.saveToLocal(value: SpotDTO(spot)))
}

func test_RecordingJourney_하위요소가_아닌_것들_저장_실패() {
XCTAssertFalse(self.journeyRepository.saveToLocal(value: Int()))
}

func test_RecordingJourney_반환_성공() {
let url = URL(string: "/../")!

let id = "id"
let startTimestamp = Date.now
let coordinate = Coordinate(latitude: 5, longitude: 5)
let spot = Spot(coordinate: coordinate, timestamp: .now, photoURL: url)

self.journeyRepository.saveToLocal(value: id)
self.journeyRepository.saveToLocal(value: Date.now)
self.journeyRepository.saveToLocal(value: SpotDTO(spot))
self.journeyRepository.saveToLocal(value: CoordinateDTO(coordinate))

guard let loadedJourney = self.journeyRepository.loadJourneyFromLocal() else {
XCTFail("load 실패")
return
}

XCTAssertEqual(loadedJourney.id, id)
XCTAssertEqual(loadedJourney.startTimestamp.description, startTimestamp.description)
XCTAssertEqual(loadedJourney.spots.description, [spot].description)
XCTAssertEqual(loadedJourney.coordinates, [coordinate])
}

}
1 change: 1 addition & 0 deletions iOS/MSFoundation/Sources/MSLogger/MSLogCategory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public enum MSLogCategory: String {
case userDefaults
case keychain = "Keychain"
case fileManager = "FileManager"
case persistable

case home
case navigateMap
Expand Down

0 comments on commit 01743cc

Please sign in to comment.