From 3e4fef9a891eed69aa60f5e5e480714e311fb5f0 Mon Sep 17 00:00:00 2001 From: Seungyeop Yeom Date: Sun, 20 Oct 2024 15:11:17 +0900 Subject: [PATCH] Store the dive site detail information --- Sources/BadaApp/AppReducer+Dependencies.swift | 8 ++- .../BadaApp/Logbook/LogbookAddReducer.swift | 24 +++++++ Sources/BadaApp/Logbook/LogbookAddSheet.swift | 37 ++++++---- .../LogbookDiveSiteSearchReducer.swift | 68 +++++++++++++++---- .../Logbook/LogbookDiveSiteSearchSheet.swift | 61 ++++++++++++++--- .../Error+Description.swift} | 10 +-- .../Models/{ => Logbook}/Companion.swift | 0 .../Models/{ => Logbook}/DiveGasType.swift | 0 .../Models/{ => Logbook}/DiveLogEntity.swift | 5 ++ .../Models/{ => Logbook}/DiveSite.swift | 17 ++--- .../Models/{ => Logbook}/DiveStyle.swift | 0 .../Models/{ => Logbook}/Feeling.swift | 0 .../BadaData/Models/{ => Logbook}/Surge.swift | 0 .../Models/{ => Logbook}/Visibility.swift | 0 .../Models/{ => Logbook}/Weather.swift | 0 .../Repositories/LocalSearchRepository.swift | 43 ++++++++++-- .../LocalSearchRepositoryType.swift | 10 ++- .../Models/LocalSearchCompletion.swift | 25 +++++++ .../BadaDomain/Models/LocalSearchResult.swift | 22 +++++- .../BadaDomain/Models/Logbook/DiveLog.swift | 3 + .../BadaDomain/Models/Logbook/DiveSite.swift | 17 ++--- .../GetLocalSearchResultUseCase.swift | 22 ++++++ .../GetLocalSearchResultsUseCase.swift | 10 +-- .../UseCases/PostDiveLogUseCase.swift | 2 +- Sources/BadaUI/KeyboardType.swift | 42 ++++++++++++ .../BadaUI/LabeledFormattedTextField.swift | 57 ++++++++++++++++ Sources/BadaUI/LabeledTextField.swift | 52 ++------------ .../BadaAppTests/LogbookAddReducerTests.swift | 30 ++++++++ 28 files changed, 447 insertions(+), 118 deletions(-) rename Sources/{BadaApp/Logbook/Models/LocalSearchResultItem.swift => BadaCore/Error+Description.swift} (51%) rename Sources/BadaData/Models/{ => Logbook}/Companion.swift (100%) rename Sources/BadaData/Models/{ => Logbook}/DiveGasType.swift (100%) rename Sources/BadaData/Models/{ => Logbook}/DiveLogEntity.swift (97%) rename Sources/BadaData/Models/{ => Logbook}/DiveSite.swift (73%) rename Sources/BadaData/Models/{ => Logbook}/DiveStyle.swift (100%) rename Sources/BadaData/Models/{ => Logbook}/Feeling.swift (100%) rename Sources/BadaData/Models/{ => Logbook}/Surge.swift (100%) rename Sources/BadaData/Models/{ => Logbook}/Visibility.swift (100%) rename Sources/BadaData/Models/{ => Logbook}/Weather.swift (100%) create mode 100644 Sources/BadaDomain/Models/LocalSearchCompletion.swift create mode 100644 Sources/BadaDomain/UseCases/GetLocalSearchResultUseCase.swift create mode 100644 Sources/BadaUI/KeyboardType.swift create mode 100644 Sources/BadaUI/LabeledFormattedTextField.swift diff --git a/Sources/BadaApp/AppReducer+Dependencies.swift b/Sources/BadaApp/AppReducer+Dependencies.swift index 8759166..703d228 100644 --- a/Sources/BadaApp/AppReducer+Dependencies.swift +++ b/Sources/BadaApp/AppReducer+Dependencies.swift @@ -23,11 +23,17 @@ extension AppReducer { } } register { - GetLocalSearchResultsUseCase { searchText in + GetLocalSearchCompletionsUseCase { searchText in let repository = LocalSearchRepository() return await repository.search(text: searchText) } } + register { + GetLocalSearchResultUseCase { searchCompletion throws(LocalSearchRepositoryError) in + let repository = LocalSearchRepository() + return try await repository.search(for: searchCompletion) + } + } } private func register( diff --git a/Sources/BadaApp/Logbook/LogbookAddReducer.swift b/Sources/BadaApp/Logbook/LogbookAddReducer.swift index 90ce604..cc451de 100644 --- a/Sources/BadaApp/Logbook/LogbookAddReducer.swift +++ b/Sources/BadaApp/Logbook/LogbookAddReducer.swift @@ -12,6 +12,8 @@ struct LogbookAddReducer: Reducer { enum Action: Sendable { case setLogNumber(Int?) case setLogDate(Date) + case setDiveSite(LocalSearchResult) + case setDiveCenter(String) case setDiveStyle(DiveStyle) case setEntryTime(Date) case setExitTime(Date) @@ -33,6 +35,8 @@ struct LogbookAddReducer: Reducer { struct State: Sendable, Equatable { var logNumber: Int? var logDate: Date = Date(timeIntervalSince1970: 0) + var diveSite: DiveSite? + var diveCenter: String = "" var diveStyle: DiveStyle = .boat var entryTime: Date = Date(timeIntervalSince1970: 0) var exitTime: Date = Date(timeIntervalSince1970: 0) @@ -59,6 +63,26 @@ struct LogbookAddReducer: Reducer { case let .setLogDate(logDate): state.logDate = logDate return .none + case let .setDiveSite(searchResult): + if let coordinate = searchResult.coordinate { + state.diveSite = DiveSite( + title: searchResult.title, + subtitle: searchResult.subtitle, + coordinate: DiveSite.Coordinate( + latitude: coordinate.latitude, + longitude: coordinate.longitude + ) + ) + } else { + state.diveSite = DiveSite( + title: searchResult.title, + subtitle: searchResult.subtitle + ) + } + return .none + case let .setDiveCenter(diveCenter): + state.diveCenter = diveCenter + return .none case let .setDiveStyle(diveStyle): state.diveStyle = diveStyle return .none diff --git a/Sources/BadaApp/Logbook/LogbookAddSheet.swift b/Sources/BadaApp/Logbook/LogbookAddSheet.swift index fcda6c4..2f90bda 100644 --- a/Sources/BadaApp/Logbook/LogbookAddSheet.swift +++ b/Sources/BadaApp/Logbook/LogbookAddSheet.swift @@ -22,7 +22,7 @@ struct LogbookAddSheet: View { NavigationStack { Form { Section { - LabeledTextField( + LabeledFormattedTextField( value: store.binding(\.logNumber, send: { .setLogNumber($0) }), format: .number, prompt: "123", @@ -36,12 +36,20 @@ struct LogbookAddSheet: View { displayedComponents: .date ) LabeledContent("Dive site") { - Text("Bohol") + Text(store.state.diveSite?.title ?? "search") + .foregroundStyle(store.state.diveSite == nil ? .tertiary : .secondary) } .contentShape(Rectangle()) .onTapGesture { store.send(.setIsDiveSiteSearchSheetPresenting(true)) } + LabeledTextField( + value: store.binding(\.diveCenter, send: { .setDiveCenter($0) }), + prompt: "name", + label: "Dive center", + keyboardType: .default + ) + .focused($focusedField, equals: .diveCenter) Picker( "Dive style", selection: store.binding(\.diveStyle, send: { .setDiveStyle($0) }) @@ -62,7 +70,7 @@ struct LogbookAddSheet: View { selection: store.binding(\.exitTime, send: { .setExitTime($0) }), displayedComponents: .hourAndMinute ) - LabeledTextField( + LabeledFormattedTextField( value: Binding( get: { store.state.bottomTime?.rawValue }, set: { store.send(.setBottomTime($0)) }), @@ -72,7 +80,7 @@ struct LogbookAddSheet: View { keyboardType: .decimalPad ) .focused($focusedField, equals: .bottomTime) - LabeledTextField( + LabeledFormattedTextField( value: Binding( get: { store.state.surfaceInterval?.rawValue }, set: { store.send(.setSurfaceInterval($0)) }), @@ -84,7 +92,7 @@ struct LogbookAddSheet: View { .focused($focusedField, equals: .surfaceInterval) } Section(header: Text("Air pressure")) { - LabeledTextField( + LabeledFormattedTextField( value: Binding( get: { store.state.entryAir?.rawValue }, set: { store.send(.setEntryAir($0)) }), @@ -94,7 +102,7 @@ struct LogbookAddSheet: View { keyboardType: .numberPad ) .focused($focusedField, equals: .entryAir) - LabeledTextField( + LabeledFormattedTextField( value: Binding( get: { store.state.exitAir?.rawValue }, set: { store.send(.setExitAir($0)) }), @@ -106,7 +114,7 @@ struct LogbookAddSheet: View { .focused($focusedField, equals: .exitAir) } Section(header: Text("Depth")) { - LabeledTextField( + LabeledFormattedTextField( value: Binding( get: { store.state.maximumDepth?.rawValue }, set: { store.send(.setMaximumDepth($0)) }), @@ -116,7 +124,7 @@ struct LogbookAddSheet: View { keyboardType: .numberPad ) .focused($focusedField, equals: .maximumDepth) - LabeledTextField( + LabeledFormattedTextField( value: Binding( get: { store.state.averageDepth?.rawValue }, set: { store.send(.setAverageDepth($0)) }), @@ -128,7 +136,7 @@ struct LogbookAddSheet: View { .focused($focusedField, equals: .averageDepth) } Section(header: Text("Temperature")) { - LabeledTextField( + LabeledFormattedTextField( value: Binding( get: { store.state.airTemperature?.rawValue }, set: { store.send(.setAirTemperature($0)) }), @@ -138,7 +146,7 @@ struct LogbookAddSheet: View { keyboardType: .numberPad ) .focused($focusedField, equals: .maximumWaterTemperature) - LabeledTextField( + LabeledFormattedTextField( value: Binding( get: { store.state.surfaceTemperature?.rawValue }, set: { store.send(.setSurfaceTemperature($0)) }), @@ -148,7 +156,7 @@ struct LogbookAddSheet: View { keyboardType: .numberPad ) .focused($focusedField, equals: .minimumWaterTemperature) - LabeledTextField( + LabeledFormattedTextField( value: Binding( get: { store.state.bottomTemperature?.rawValue }, set: { store.send(.setBottomTemperature($0)) }), @@ -226,7 +234,7 @@ struct LogbookAddSheet: View { get: { store.state.isDiveSiteSearchSheetPresenting }, set: { store.send(.setIsDiveSiteSearchSheetPresenting($0)) } ), - content: { LogbookDiveSiteSearchSheet() } + content: { LogbookDiveSiteSearchSheet(action: selectDiveSite) } ) #if os(macOS) .padding() @@ -275,6 +283,10 @@ struct LogbookAddSheet: View { dismiss() } + private func selectDiveSite(_ searchResult: LocalSearchResult) { + store.send(.setDiveSite(searchResult)) + } + private func tapDoneButton() { focusedField = nil } @@ -291,6 +303,7 @@ struct LogbookAddSheet: View { extension LogbookAddSheet { private enum Field: Int, CaseIterable { case logNumber = 0 + case diveCenter case bottomTime case surfaceInterval case entryAir diff --git a/Sources/BadaApp/Logbook/LogbookDiveSiteSearchReducer.swift b/Sources/BadaApp/Logbook/LogbookDiveSiteSearchReducer.swift index 383696f..3b24965 100644 --- a/Sources/BadaApp/Logbook/LogbookDiveSiteSearchReducer.swift +++ b/Sources/BadaApp/Logbook/LogbookDiveSiteSearchReducer.swift @@ -10,35 +10,77 @@ import BadaDomain struct LogbookDiveSiteSearchReducer: Reducer { enum Action: Sendable { + case search(for: LocalSearchCompletion) + case setSearchResult(LocalSearchResult?) case setSearchText(String) - case setSearchResults([LocalSearchResultItem]) + case setSearchCompletions([LocalSearchCompletion]) + case setIsSearching(Bool) } struct State: Sendable, Equatable { var searchText: String = "" - var searchResults: [LocalSearchResultItem] = [] + var searchCompletions: [LocalSearchCompletion] = [] + var searchResult: LocalSearchResult? + var isSearching: Bool = false } - @UseCase private var getLocalSearchResultsUseCase: GetLocalSearchResultsUseCase + @UseCase private var getLocalSearchCompletionsUseCase: GetLocalSearchCompletionsUseCase + @UseCase private var getLocalSearchResultUseCase: GetLocalSearchResultUseCase func reduce(state: inout State, action: Action) -> AnyEffect { switch action { + case let .search(searchCompletion): + return .concat( + .just(.setIsSearching(true)), + .single { await executeGetLocalSearchResultUseCase(for: searchCompletion) }, + .just(.setIsSearching(false)) + ) + case let .setSearchResult(searchResult): + state.searchResult = searchResult + return .none case let .setSearchText(searchText): state.searchText = searchText return .single { - let searchResults = await getLocalSearchResultsUseCase.execute(searchText: searchText) - .map { result in - LocalSearchResultItem( - title: result.title, - subtitle: result.subtitle - ) - } + let searchCompletions = await getLocalSearchCompletionsUseCase.execute(for: searchText) .sorted { $0.title < $1.title } - return .setSearchResults(searchResults) + return .setSearchCompletions(searchCompletions) + } + case let .setSearchCompletions(searchCompletions): + if searchCompletions.isEmpty { + let manualCompletion = LocalSearchCompletion( + title: state.searchText, + subtitle: "No matching results", + rawValue: nil + ) + state.searchCompletions = [manualCompletion] + } else { + state.searchCompletions = searchCompletions } - case let .setSearchResults(searchResults): - state.searchResults = searchResults return .none + case let .setIsSearching(isSearching): + state.isSearching = isSearching + return .none + } + } + + private func executeGetLocalSearchResultUseCase(for searchCompletion: LocalSearchCompletion) async -> Action { + do { + let searchResult = try await getLocalSearchResultUseCase.execute(for: searchCompletion) + return .setSearchResult(searchResult) + } catch { + switch error { + case .invalidSearchCompletion: + let searchResult = LocalSearchResult( + title: searchCompletion.title, + subtitle: searchCompletion.subtitle, + coordinate: nil + ) + return .setSearchResult(searchResult) + case .searchFailed, + .searchCompletionNotFound, + .mapItemNotFound: + return .setSearchResult(nil) + } } } } diff --git a/Sources/BadaApp/Logbook/LogbookDiveSiteSearchSheet.swift b/Sources/BadaApp/Logbook/LogbookDiveSiteSearchSheet.swift index cbfe63a..b30e50f 100644 --- a/Sources/BadaApp/Logbook/LogbookDiveSiteSearchSheet.swift +++ b/Sources/BadaApp/Logbook/LogbookDiveSiteSearchSheet.swift @@ -6,9 +6,13 @@ // import BadaCore +import BadaDomain import BadaUI struct LogbookDiveSiteSearchSheet: View { + let action: (LocalSearchResult) -> Void + + @Environment(\.dismiss) private var dismiss @StateObject private var store = ViewStore( reducer: LogbookDiveSiteSearchReducer(), state: LogbookDiveSiteSearchReducer.State() @@ -16,23 +20,62 @@ struct LogbookDiveSiteSearchSheet: View { var body: some View { NavigationStack { - List(store.state.searchResults, id: \.self) { result in - VStack(alignment: .leading) { - Text(result.title) - .font(.headline) - .foregroundStyle(.primary) - if !result.subtitle.isEmpty { - Text(result.subtitle) - .font(.subheadline) - .foregroundStyle(.secondary) + List(Array(store.state.searchCompletions.enumerated()), id: \.offset) { index, searchCompletion in + Button(action: { tapItem(searchCompletion) }) { + VStack(alignment: .leading) { + Text(searchCompletion.title) + .font(.headline) + .foregroundStyle(Color.primary) + if !searchCompletion.subtitle.isEmpty { + Text(searchCompletion.subtitle) + .font(.subheadline) + .foregroundStyle(Color.secondary) + } } } } .navigationTitle("Dive site") + #if os(macOS) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + cancelButton + } + } + .frame(idealWidth: 400, idealHeight: 600) + #endif .searchable( text: store.binding(\.searchText, send: { .setSearchText($0) }), prompt: Text("Search") ) + .disabled(store.state.isSearching) + .overlay { + if store.state.isSearching { + ProgressView() + .progressViewStyle(.circular) + .controlSize(.large) + } + } + .onChange(of: store.state.searchResult, searchResultChanged) + } + } + + private var cancelButton: some View { + Button(action: tapCancelButton) { + Text("Cancel") } } + + private func searchResultChanged() { + guard let searchResult = store.state.searchResult else { return } + action(searchResult) + dismiss() + } + + private func tapItem(_ item: LocalSearchCompletion) { + store.send(.search(for: item)) + } + + private func tapCancelButton() { + dismiss() + } } diff --git a/Sources/BadaApp/Logbook/Models/LocalSearchResultItem.swift b/Sources/BadaCore/Error+Description.swift similarity index 51% rename from Sources/BadaApp/Logbook/Models/LocalSearchResultItem.swift rename to Sources/BadaCore/Error+Description.swift index c186b8d..3c23ea0 100644 --- a/Sources/BadaApp/Logbook/Models/LocalSearchResultItem.swift +++ b/Sources/BadaCore/Error+Description.swift @@ -5,10 +5,10 @@ // Copyright (c) 2024 Seungyeop Yeom ( https://github.com/OceanPositive ). // -import BadaCore +import Foundation -struct LocalSearchResultItem: Hashable { - let id = UUID() - let title: String - let subtitle: String +extension Error { + package var description: String { + "\(localizedDescription): \(self)" + } } diff --git a/Sources/BadaData/Models/Companion.swift b/Sources/BadaData/Models/Logbook/Companion.swift similarity index 100% rename from Sources/BadaData/Models/Companion.swift rename to Sources/BadaData/Models/Logbook/Companion.swift diff --git a/Sources/BadaData/Models/DiveGasType.swift b/Sources/BadaData/Models/Logbook/DiveGasType.swift similarity index 100% rename from Sources/BadaData/Models/DiveGasType.swift rename to Sources/BadaData/Models/Logbook/DiveGasType.swift diff --git a/Sources/BadaData/Models/DiveLogEntity.swift b/Sources/BadaData/Models/Logbook/DiveLogEntity.swift similarity index 97% rename from Sources/BadaData/Models/DiveLogEntity.swift rename to Sources/BadaData/Models/Logbook/DiveLogEntity.swift index 243faa8..11f0d0a 100644 --- a/Sources/BadaData/Models/DiveLogEntity.swift +++ b/Sources/BadaData/Models/Logbook/DiveLogEntity.swift @@ -16,6 +16,7 @@ final class DiveLogEntity { var logNumber: Int var logDate: Date? var diveSite: DiveSite? + var diveCenter: String? var diveStyle: DiveStyle? var entryTime: Date? var exitTime: Date? @@ -44,6 +45,7 @@ final class DiveLogEntity { logNumber: Int, logDate: Date? = nil, diveSite: DiveSite? = nil, + diveCenter: String? = nil, diveStyle: DiveStyle? = nil, entryTime: Date? = nil, exitTime: Date? = nil, @@ -71,6 +73,7 @@ final class DiveLogEntity { self.logNumber = logNumber self.logDate = logDate self.diveSite = diveSite + self.diveCenter = diveCenter self.diveStyle = diveStyle self.entryTime = entryTime self.exitTime = exitTime @@ -103,6 +106,7 @@ extension DiveLogEntity: DomainConvertible { logNumber: logNumber, logDate: logDate, diveSite: diveSite?.domain, + diveCenter: diveCenter, diveStyle: diveStyle?.domain, entryTime: entryTime, exitTime: exitTime, @@ -134,6 +138,7 @@ extension DiveLogEntity: DomainConvertible { logNumber: domain.logNumber, logDate: domain.logDate, diveSite: domain.diveSite.map { DiveSite(domain: $0) }, + diveCenter: domain.diveCenter, diveStyle: domain.diveStyle.map { DiveStyle(domain: $0) }, entryTime: domain.entryTime, exitTime: domain.exitTime, diff --git a/Sources/BadaData/Models/DiveSite.swift b/Sources/BadaData/Models/Logbook/DiveSite.swift similarity index 73% rename from Sources/BadaData/Models/DiveSite.swift rename to Sources/BadaData/Models/Logbook/DiveSite.swift index 3261273..54d7883 100644 --- a/Sources/BadaData/Models/DiveSite.swift +++ b/Sources/BadaData/Models/Logbook/DiveSite.swift @@ -9,10 +9,9 @@ import BadaCore import BadaDomain struct DiveSite: Codable { + let title: String + let subtitle: String let coordinate: Coordinate? - let country: String - let siteName: String - let diveCenter: String? struct Coordinate: Codable { let latitude: Double @@ -23,27 +22,25 @@ struct DiveSite: Codable { extension DiveSite: DomainConvertible { var domain: BadaDomain.DiveSite { BadaDomain.DiveSite( + title: title, + subtitle: subtitle, coordinate: coordinate.map { BadaDomain.DiveSite.Coordinate( latitude: $0.latitude, longitude: $0.longitude ) - }, - country: country, - siteName: siteName, - diveCenter: diveCenter + } ) } init(domain: BadaDomain.DiveSite) { + self.title = domain.title + self.subtitle = domain.subtitle self.coordinate = domain.coordinate.map { DiveSite.Coordinate( latitude: $0.latitude, longitude: $0.longitude ) } - self.country = domain.country - self.siteName = domain.siteName - self.diveCenter = domain.diveCenter } } diff --git a/Sources/BadaData/Models/DiveStyle.swift b/Sources/BadaData/Models/Logbook/DiveStyle.swift similarity index 100% rename from Sources/BadaData/Models/DiveStyle.swift rename to Sources/BadaData/Models/Logbook/DiveStyle.swift diff --git a/Sources/BadaData/Models/Feeling.swift b/Sources/BadaData/Models/Logbook/Feeling.swift similarity index 100% rename from Sources/BadaData/Models/Feeling.swift rename to Sources/BadaData/Models/Logbook/Feeling.swift diff --git a/Sources/BadaData/Models/Surge.swift b/Sources/BadaData/Models/Logbook/Surge.swift similarity index 100% rename from Sources/BadaData/Models/Surge.swift rename to Sources/BadaData/Models/Logbook/Surge.swift diff --git a/Sources/BadaData/Models/Visibility.swift b/Sources/BadaData/Models/Logbook/Visibility.swift similarity index 100% rename from Sources/BadaData/Models/Visibility.swift rename to Sources/BadaData/Models/Logbook/Visibility.swift diff --git a/Sources/BadaData/Models/Weather.swift b/Sources/BadaData/Models/Logbook/Weather.swift similarity index 100% rename from Sources/BadaData/Models/Weather.swift rename to Sources/BadaData/Models/Logbook/Weather.swift diff --git a/Sources/BadaData/Repositories/LocalSearchRepository.swift b/Sources/BadaData/Repositories/LocalSearchRepository.swift index 25028e2..0f780f0 100644 --- a/Sources/BadaData/Repositories/LocalSearchRepository.swift +++ b/Sources/BadaData/Repositories/LocalSearchRepository.swift @@ -5,33 +5,68 @@ // Copyright (c) 2024 Seungyeop Yeom ( https://github.com/OceanPositive ). // +import BadaCore import BadaDomain import MapKit package final class LocalSearchRepository: NSObject, LocalSearchRepositoryType { private let completer = MKLocalSearchCompleter() - private var searchContinuation: CheckedContinuation<[LocalSearchResult], Never>? + private var searchContinuation: CheckedContinuation<[LocalSearchCompletion], Never>? package override init() { super.init() completer.delegate = self + completer.resultTypes = MKLocalSearchCompleter.ResultType(rawValue: 0) } @MainActor - package func search(text: String) async -> [LocalSearchResult] { + package func search(text: String) async -> [LocalSearchCompletion] { return await withCheckedContinuation { continuation in searchContinuation = continuation completer.queryFragment = text } } + + package func search(for searchCompletion: LocalSearchCompletion) async throws(LocalSearchRepositoryError) -> LocalSearchResult { + guard let searchCompletion = searchCompletion.rawValue else { + throw LocalSearchRepositoryError.invalidSearchCompletion + } + let request = MKLocalSearch.Request(completion: searchCompletion) + let search = MKLocalSearch(request: request) + do { + let response = try await search.start() + guard let item = response.mapItems.first else { + throw LocalSearchRepositoryError.mapItemNotFound + } + if let location = item.placemark.location { + return LocalSearchResult( + title: searchCompletion.title, + subtitle: searchCompletion.subtitle, + coordinate: LocalSearchResult.Coordinate( + latitude: location.coordinate.latitude, + longitude: location.coordinate.longitude + ) + ) + } else { + return LocalSearchResult( + title: searchCompletion.title, + subtitle: searchCompletion.subtitle, + coordinate: nil + ) + } + } catch { + throw LocalSearchRepositoryError.searchFailed(error.description) + } + } } extension LocalSearchRepository: MKLocalSearchCompleterDelegate { package func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { let searchResults = completer.results.map { result in - LocalSearchResult( + LocalSearchCompletion( title: result.title, - subtitle: result.subtitle + subtitle: result.subtitle, + rawValue: result ) } searchContinuation?.resume(returning: searchResults) diff --git a/Sources/BadaDomain/Interfaces/LocalSearchRepositoryType.swift b/Sources/BadaDomain/Interfaces/LocalSearchRepositoryType.swift index 84fd6af..00068f0 100644 --- a/Sources/BadaDomain/Interfaces/LocalSearchRepositoryType.swift +++ b/Sources/BadaDomain/Interfaces/LocalSearchRepositoryType.swift @@ -8,5 +8,13 @@ import BadaCore package protocol LocalSearchRepositoryType { - func search(text: String) async -> [LocalSearchResult] + func search(text: String) async -> [LocalSearchCompletion] + func search(for result: LocalSearchCompletion) async throws(LocalSearchRepositoryError) -> LocalSearchResult +} + +package enum LocalSearchRepositoryError: Error { + case searchFailed(String) + case searchCompletionNotFound + case mapItemNotFound + case invalidSearchCompletion } diff --git a/Sources/BadaDomain/Models/LocalSearchCompletion.swift b/Sources/BadaDomain/Models/LocalSearchCompletion.swift new file mode 100644 index 0000000..dc003f4 --- /dev/null +++ b/Sources/BadaDomain/Models/LocalSearchCompletion.swift @@ -0,0 +1,25 @@ +// +// BadaBook +// Apache License, Version 2.0 +// +// Copyright (c) 2024 Seungyeop Yeom ( https://github.com/OceanPositive ). +// + +import BadaCore +import MapKit + +package struct LocalSearchCompletion: Equatable, @unchecked Sendable { + package let title: String + package let subtitle: String + package let rawValue: MKLocalSearchCompletion? + + package init( + title: String, + subtitle: String, + rawValue: MKLocalSearchCompletion? + ) { + self.title = title + self.subtitle = subtitle + self.rawValue = rawValue + } +} diff --git a/Sources/BadaDomain/Models/LocalSearchResult.swift b/Sources/BadaDomain/Models/LocalSearchResult.swift index 7115100..49275a1 100644 --- a/Sources/BadaDomain/Models/LocalSearchResult.swift +++ b/Sources/BadaDomain/Models/LocalSearchResult.swift @@ -5,15 +5,35 @@ // Copyright (c) 2024 Seungyeop Yeom ( https://github.com/OceanPositive ). // +import BadaCore + package struct LocalSearchResult: Equatable { package let title: String package let subtitle: String + package let coordinate: Coordinate? package init( title: String, - subtitle: String + subtitle: String, + coordinate: Coordinate? ) { self.title = title self.subtitle = subtitle + self.coordinate = coordinate + } +} + +extension LocalSearchResult { + package struct Coordinate: Equatable { + package let latitude: Double + package let longitude: Double + + package init( + latitude: Double, + longitude: Double + ) { + self.latitude = latitude + self.longitude = longitude + } } } diff --git a/Sources/BadaDomain/Models/Logbook/DiveLog.swift b/Sources/BadaDomain/Models/Logbook/DiveLog.swift index af4ca5b..4555446 100644 --- a/Sources/BadaDomain/Models/Logbook/DiveLog.swift +++ b/Sources/BadaDomain/Models/Logbook/DiveLog.swift @@ -12,6 +12,7 @@ package struct DiveLog: Equatable { package let logNumber: Int package let logDate: Date? package let diveSite: DiveSite? + package let diveCenter: String? package let diveStyle: DiveStyle? package let entryTime: Date? package let exitTime: Date? @@ -40,6 +41,7 @@ package struct DiveLog: Equatable { logNumber: Int, logDate: Date? = nil, diveSite: DiveSite? = nil, + diveCenter: String? = nil, diveStyle: DiveStyle? = nil, entryTime: Date? = nil, exitTime: Date? = nil, @@ -67,6 +69,7 @@ package struct DiveLog: Equatable { self.logNumber = logNumber self.logDate = logDate self.diveSite = diveSite + self.diveCenter = diveCenter self.diveStyle = diveStyle self.entryTime = entryTime self.exitTime = exitTime diff --git a/Sources/BadaDomain/Models/Logbook/DiveSite.swift b/Sources/BadaDomain/Models/Logbook/DiveSite.swift index a865d9a..db4e116 100644 --- a/Sources/BadaDomain/Models/Logbook/DiveSite.swift +++ b/Sources/BadaDomain/Models/Logbook/DiveSite.swift @@ -8,21 +8,18 @@ import BadaCore package struct DiveSite: Equatable { + package let title: String + package let subtitle: String package let coordinate: Coordinate? - package let country: String - package let siteName: String - package let diveCenter: String? package init( - coordinate: Coordinate? = nil, - country: String, - siteName: String, - diveCenter: String? + title: String, + subtitle: String, + coordinate: Coordinate? = nil ) { + self.title = title + self.subtitle = subtitle self.coordinate = coordinate - self.country = country - self.siteName = siteName - self.diveCenter = diveCenter } package struct Coordinate: Equatable { diff --git a/Sources/BadaDomain/UseCases/GetLocalSearchResultUseCase.swift b/Sources/BadaDomain/UseCases/GetLocalSearchResultUseCase.swift new file mode 100644 index 0000000..cb09914 --- /dev/null +++ b/Sources/BadaDomain/UseCases/GetLocalSearchResultUseCase.swift @@ -0,0 +1,22 @@ +// +// BadaBook +// Apache License, Version 2.0 +// +// Copyright (c) 2024 Seungyeop Yeom ( https://github.com/OceanPositive ). +// + +import BadaCore + +package struct GetLocalSearchResultUseCase: ExecutableUseCase { + private let get: @Sendable (LocalSearchCompletion) async throws(LocalSearchRepositoryError) -> LocalSearchResult + + package init( + _ get: @Sendable @escaping (LocalSearchCompletion) async throws(LocalSearchRepositoryError) -> LocalSearchResult + ) { + self.get = get + } + + package func execute(for searchCompletion: LocalSearchCompletion) async throws(LocalSearchRepositoryError) -> LocalSearchResult { + try await get(searchCompletion) + } +} diff --git a/Sources/BadaDomain/UseCases/GetLocalSearchResultsUseCase.swift b/Sources/BadaDomain/UseCases/GetLocalSearchResultsUseCase.swift index 47aad77..0fe06e0 100644 --- a/Sources/BadaDomain/UseCases/GetLocalSearchResultsUseCase.swift +++ b/Sources/BadaDomain/UseCases/GetLocalSearchResultsUseCase.swift @@ -5,16 +5,18 @@ // Copyright (c) 2024 Seungyeop Yeom ( https://github.com/OceanPositive ). // -package struct GetLocalSearchResultsUseCase: ExecutableUseCase { - private let get: @Sendable (String) async -> [LocalSearchResult] +import BadaCore + +package struct GetLocalSearchCompletionsUseCase: ExecutableUseCase { + private let get: @Sendable (String) async -> [LocalSearchCompletion] package init( - _ get: @Sendable @escaping (String) async -> [LocalSearchResult] + _ get: @Sendable @escaping (String) async -> [LocalSearchCompletion] ) { self.get = get } - package func execute(searchText: String) async -> [LocalSearchResult] { + package func execute(for searchText: String) async -> [LocalSearchCompletion] { await get(searchText) } } diff --git a/Sources/BadaDomain/UseCases/PostDiveLogUseCase.swift b/Sources/BadaDomain/UseCases/PostDiveLogUseCase.swift index 8109919..1251c5f 100644 --- a/Sources/BadaDomain/UseCases/PostDiveLogUseCase.swift +++ b/Sources/BadaDomain/UseCases/PostDiveLogUseCase.swift @@ -16,7 +16,7 @@ package struct PostDiveLogUseCase: ExecutableUseCase { self.post = post } - package func execute(request: DiveLogInsertRequest) async -> Result { + package func execute(for request: DiveLogInsertRequest) async -> Result { await post(request) } } diff --git a/Sources/BadaUI/KeyboardType.swift b/Sources/BadaUI/KeyboardType.swift new file mode 100644 index 0000000..0d241ad --- /dev/null +++ b/Sources/BadaUI/KeyboardType.swift @@ -0,0 +1,42 @@ +// +// BadaBook +// Apache License, Version 2.0 +// +// Copyright (c) 2024 Seungyeop Yeom ( https://github.com/OceanPositive ). +// + +import SwiftUI + +package enum KeyboardType: Int, Sendable { + case `default` = 0 + case asciiCapable = 1 + case numbersAndPunctuation = 2 + case url = 3 + case numberPad = 4 + case phonePad = 5 + case namePhonePad = 6 + case emailAddress = 7 + case decimalPad = 8 + case twitter = 9 + case webSearch = 10 + case asciiCapableNumberPad = 11 + + #if os(iOS) + var uiKeyboardType: UIKeyboardType { + switch self { + case .default: return .default + case .asciiCapable: return .asciiCapable + case .numbersAndPunctuation: return .numbersAndPunctuation + case .url: return .URL + case .numberPad: return .numberPad + case .phonePad: return .phonePad + case .namePhonePad: return .namePhonePad + case .emailAddress: return .emailAddress + case .decimalPad: return .decimalPad + case .twitter: return .twitter + case .webSearch: return .webSearch + case .asciiCapableNumberPad: return .asciiCapableNumberPad + } + } + #endif +} diff --git a/Sources/BadaUI/LabeledFormattedTextField.swift b/Sources/BadaUI/LabeledFormattedTextField.swift new file mode 100644 index 0000000..31f5ed4 --- /dev/null +++ b/Sources/BadaUI/LabeledFormattedTextField.swift @@ -0,0 +1,57 @@ +// +// BadaBook +// Apache License, Version 2.0 +// +// Copyright (c) 2024 Seungyeop Yeom ( https://github.com/OceanPositive ). +// + +import SwiftUI + +package struct LabeledFormattedTextField: View +where Format: ParseableFormatStyle, Format.FormatOutput == String { + private let label: String + private let value: Binding + private let format: Format + private let prompt: String + private let keyboardType: KeyboardType + private let textAlignment: TextAlignment + + package init( + value: Binding, + format: Format, + prompt: String, + label: String, + keyboardType: KeyboardType, + textAlignment: TextAlignment = .trailing + ) { + self.label = label + self.value = value + self.format = format + self.prompt = prompt + self.keyboardType = keyboardType + self.textAlignment = textAlignment + } + + package var body: some View { + #if os(iOS) + LabeledContent(label) { + TextField( + value: value, + format: format, + prompt: Text(prompt), + label: { Text(label) } + ) + .multilineTextAlignment(textAlignment) + .keyboardType(keyboardType.uiKeyboardType) + } + #else + TextField( + value: value, + format: format, + prompt: Text(prompt), + label: { Text(label) } + ) + .multilineTextAlignment(textAlignment) + #endif + } +} diff --git a/Sources/BadaUI/LabeledTextField.swift b/Sources/BadaUI/LabeledTextField.swift index d677791..49ce252 100644 --- a/Sources/BadaUI/LabeledTextField.swift +++ b/Sources/BadaUI/LabeledTextField.swift @@ -7,18 +7,15 @@ import SwiftUI -package struct LabeledTextField: View -where Format: ParseableFormatStyle, Format.FormatOutput == String { +package struct LabeledTextField: View { private let label: String - private let value: Binding - private let format: Format + private let value: Binding private let prompt: String private let keyboardType: KeyboardType private let textAlignment: TextAlignment package init( - value: Binding, - format: Format, + value: Binding, prompt: String, label: String, keyboardType: KeyboardType, @@ -26,7 +23,6 @@ where Format: ParseableFormatStyle, Format.FormatOutput == String { ) { self.label = label self.value = value - self.format = format self.prompt = prompt self.keyboardType = keyboardType self.textAlignment = textAlignment @@ -36,8 +32,7 @@ where Format: ParseableFormatStyle, Format.FormatOutput == String { #if os(iOS) LabeledContent(label) { TextField( - value: value, - format: format, + text: value, prompt: Text(prompt), label: { Text(label) } ) @@ -46,8 +41,7 @@ where Format: ParseableFormatStyle, Format.FormatOutput == String { } #else TextField( - value: value, - format: format, + text: value, prompt: Text(prompt), label: { Text(label) } ) @@ -55,39 +49,3 @@ where Format: ParseableFormatStyle, Format.FormatOutput == String { #endif } } - -extension LabeledTextField { - package enum KeyboardType: Int, Sendable { - case `default` = 0 - case asciiCapable = 1 - case numbersAndPunctuation = 2 - case url = 3 - case numberPad = 4 - case phonePad = 5 - case namePhonePad = 6 - case emailAddress = 7 - case decimalPad = 8 - case twitter = 9 - case webSearch = 10 - case asciiCapableNumberPad = 11 - - #if os(iOS) - var uiKeyboardType: UIKeyboardType { - switch self { - case .default: return .default - case .asciiCapable: return .asciiCapable - case .numbersAndPunctuation: return .numbersAndPunctuation - case .url: return .URL - case .numberPad: return .numberPad - case .phonePad: return .phonePad - case .namePhonePad: return .namePhonePad - case .emailAddress: return .emailAddress - case .decimalPad: return .decimalPad - case .twitter: return .twitter - case .webSearch: return .webSearch - case .asciiCapableNumberPad: return .asciiCapableNumberPad - } - } - #endif - } -} diff --git a/Tests/BadaAppTests/LogbookAddReducerTests.swift b/Tests/BadaAppTests/LogbookAddReducerTests.swift index 5f268a8..97c17aa 100644 --- a/Tests/BadaAppTests/LogbookAddReducerTests.swift +++ b/Tests/BadaAppTests/LogbookAddReducerTests.swift @@ -34,6 +34,36 @@ struct LogbookAddReducerTests { await sut.expect(\.logDate, Date(timeIntervalSince1970: 12)) } + @Test + func setDiveSite() async { + let sut = Store( + reducer: LogbookAddReducer(), + state: LogbookAddReducer.State() + ) + await sut.expect(\.diveSite, nil) + let searchResult = LocalSearchResult( + title: "Hello", + subtitle: "World", + coordinate: LocalSearchResult.Coordinate(latitude: 12, longitude: 13) + ) + await sut.send(.setDiveSite(searchResult)) + await sut.expect(\.diveSite?.title, searchResult.title) + await sut.expect(\.diveSite?.subtitle, searchResult.subtitle) + await sut.expect(\.diveSite?.coordinate?.latitude, searchResult.coordinate?.latitude) + await sut.expect(\.diveSite?.coordinate?.longitude, searchResult.coordinate?.longitude) + } + + @Test + func setDiveCenter() async { + let sut = Store( + reducer: LogbookAddReducer(), + state: LogbookAddReducer.State() + ) + await sut.expect(\.diveCenter, "") + await sut.send(.setDiveCenter("Hello World")) + await sut.expect(\.diveCenter, "Hello World") + } + @Test func setDiveStyle() async { let sut = Store(