From da8e0fe8fa254dfb240bdd5f912057b856d19650 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 08:02:47 +0900 Subject: [PATCH 1/5] [FIX] delete coordinator --- HotSpot/Sources/App/HotSpotApp.swift | 12 +- .../Coordinator/AppCoordinator.swift | 84 ------------- .../Coordinator/AppCoordinatorView.swift | 85 ------------- .../Sources/Presentation/Map/MapStore.swift | 39 +++++- .../Sources/Presentation/Map/MapView.swift | 42 +++++++ .../Search/Component/SearchFilterView.swift | 16 +-- .../Presentation/Search/SearchStore.swift | 12 +- .../Presentation/Search/SearchView.swift | 119 ++++++++++-------- .../ShopImageSection.swift | 0 .../ShopInfoSection.swift | 0 .../ShopLocationMapView.swift | 0 .../ShopDetail/ShopDetailStore.swift | 15 +-- .../ShopDetail/ShopDetailView.swift | 3 +- 13 files changed, 180 insertions(+), 247 deletions(-) delete mode 100644 HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift delete mode 100644 HotSpot/Sources/Presentation/Coordinator/AppCoordinatorView.swift rename HotSpot/Sources/Presentation/ShopDetail/{Components => Component}/ShopImageSection.swift (100%) rename HotSpot/Sources/Presentation/ShopDetail/{Components => Component}/ShopInfoSection.swift (100%) rename HotSpot/Sources/Presentation/ShopDetail/{Components => Component}/ShopLocationMapView.swift (100%) diff --git a/HotSpot/Sources/App/HotSpotApp.swift b/HotSpot/Sources/App/HotSpotApp.swift index 93a2769..298f34f 100644 --- a/HotSpot/Sources/App/HotSpotApp.swift +++ b/HotSpot/Sources/App/HotSpotApp.swift @@ -5,12 +5,14 @@ import ComposableArchitecture struct HotSpotApp: App { var body: some Scene { WindowGroup { - AppCoordinatorView( - store: Store( - initialState: AppCoordinator.State(), - reducer: { AppCoordinator() } + NavigationView { + MapView( + store: Store( + initialState: MapStore.State(), + reducer: { MapStore() } + ) ) - ) + } } } } \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift b/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift deleted file mode 100644 index 5edf3c1..0000000 --- a/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift +++ /dev/null @@ -1,84 +0,0 @@ -import SwiftUI -import ComposableArchitecture - -@Reducer -struct AppCoordinator { - struct State: Equatable { - var map: MapStore.State = .init() - var search: SearchStore.State? - var shopDetail: ShopDetailStore.State? - var selectedShop: ShopModel? - var isDetailPresented: Bool = false - } - - enum Action { - case map(MapStore.Action) - case search(SearchStore.Action) - case shopDetail(ShopDetailStore.Action) - - case showShopDetail(ShopModel) - case showSearch - case dismissSearch - case dismissDetail - } - - var body: some ReducerOf { - Scope(state: \.map, action: \.map) { - MapStore() - } - - Reduce { state, action in - switch action { - case .map(.showSearch), .showSearch: - state.search = .init() - return .none - - case .search(.pop), .dismissSearch: - state.search = nil - return .none - - case let .search(.selectShop(shop)): - state.selectedShop = shop - state.isDetailPresented = true - return .send(.showShopDetail(shop)) - - case let .showShopDetail(shop): - state.shopDetail = .init(shop: shop) - state.isDetailPresented = true - return .none - - case .shopDetail(.pop): - state.shopDetail = nil - state.selectedShop = nil - state.isDetailPresented = false - return .none - - case .dismissDetail: - state.shopDetail = nil - state.selectedShop = nil - state.isDetailPresented = false - return .none - - case let .map(.showShopDetail(shop)): - state.selectedShop = shop - state.isDetailPresented = true - return .send(.showShopDetail(shop)) - - case .map: - return .none - - case .search: - return .none - - case .shopDetail: - return .none - } - } - .ifLet(\.search, action: \.search) { - SearchStore() - } - .ifLet(\.shopDetail, action: \.shopDetail) { - ShopDetailStore() - } - } -} diff --git a/HotSpot/Sources/Presentation/Coordinator/AppCoordinatorView.swift b/HotSpot/Sources/Presentation/Coordinator/AppCoordinatorView.swift deleted file mode 100644 index 160b678..0000000 --- a/HotSpot/Sources/Presentation/Coordinator/AppCoordinatorView.swift +++ /dev/null @@ -1,85 +0,0 @@ -import SwiftUI -import ComposableArchitecture - -struct AppCoordinatorView: View { - let store: StoreOf - - var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in - NavigationView { - MapView(store: store.scope(state: \.map, action: \.map)) - .background( - Group { - searchNavigationLink(viewStore: viewStore) - mapToDetailNavigationLink(viewStore: viewStore) - } - ) - } - .navigationViewStyle(.stack) - } - } - - private func searchNavigationLink(viewStore: ViewStore) -> some View { - NavigationLink( - destination: IfLetStore( - store.scope(state: \.search, action: \.search), - then: { store in - SearchView(store: store, coordinatorStore: self.store) - .background(searchToDetailNavigationLink(viewStore: viewStore)) - } - ), - isActive: viewStore.binding( - get: { $0.search != nil }, - send: { $0 ? .showSearch : .dismissSearch } - ) - ) { - EmptyView() - } - .hidden() - } - - private func mapToDetailNavigationLink(viewStore: ViewStore) -> some View { - NavigationLink( - destination: IfLetStore( - store.scope(state: \.shopDetail, action: \.shopDetail), - then: { store in - ShopDetailView(store: store) - } - ), - isActive: viewStore.binding( - get: { $0.isDetailPresented && $0.search == nil }, - send: { $0 ? .showShopDetail(viewStore.selectedShop!) : .dismissDetail } - ) - ) { - EmptyView() - } - .hidden() - } - - private func searchToDetailNavigationLink(viewStore: ViewStore) -> some View { - NavigationLink( - destination: IfLetStore( - store.scope(state: \.shopDetail, action: \.shopDetail), - then: { store in - ShopDetailView(store: store) - } - ), - isActive: viewStore.binding( - get: { $0.isDetailPresented && $0.search != nil }, - send: { $0 ? .showShopDetail(viewStore.selectedShop!) : .dismissDetail } - ) - ) { - EmptyView() - } - .hidden() - } -} - -#Preview { - AppCoordinatorView( - store: Store( - initialState: AppCoordinator.State(), - reducer: { AppCoordinator() } - ) - ) -} diff --git a/HotSpot/Sources/Presentation/Map/MapStore.swift b/HotSpot/Sources/Presentation/Map/MapStore.swift index 832661f..31e81c8 100644 --- a/HotSpot/Sources/Presentation/Map/MapStore.swift +++ b/HotSpot/Sources/Presentation/Map/MapStore.swift @@ -17,6 +17,33 @@ struct MapStore { ) var error: String? = nil var lastFetchedLocation: CLLocationCoordinate2D? = nil + var navigationPath: [NavigationDestination] = [] + } + + enum NavigationDestination: Hashable { + case search + case shopDetail(ShopModel) + + func hash(into hasher: inout Hasher) { + switch self { + case .search: + hasher.combine("search") + case .shopDetail(let shop): + hasher.combine("shopDetail") + hasher.combine(shop.id) + } + } + + static func == (lhs: NavigationDestination, rhs: NavigationDestination) -> Bool { + switch (lhs, rhs) { + case (.search, .search): + return true + case (.shopDetail(let lhsShop), .shopDetail(let rhsShop)): + return lhsShop.id == rhsShop.id + default: + return false + } + } } enum Action { @@ -26,6 +53,7 @@ struct MapStore { case handleError(Error) case showSearch case showShopDetail(ShopModel) + case pop } var body: some ReducerOf { @@ -66,10 +94,18 @@ struct MapStore { return .none case .showSearch: + state.navigationPath.append(.search) + return .none + + case .pop: + if !state.navigationPath.isEmpty { + state.navigationPath.removeLast() + } return .none case let .showShopDetail(shop): state.selectedShop = shop + state.navigationPath.append(.shopDetail(shop)) return .none } } @@ -115,6 +151,7 @@ extension MapStore.State { lhs.region.span.longitudeDelta == rhs.region.span.longitudeDelta && lhs.error == rhs.error && lhs.lastFetchedLocation?.latitude == rhs.lastFetchedLocation?.latitude && - lhs.lastFetchedLocation?.longitude == rhs.lastFetchedLocation?.longitude + lhs.lastFetchedLocation?.longitude == rhs.lastFetchedLocation?.longitude && + lhs.navigationPath == rhs.navigationPath } } diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index 5879f25..d75e954 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -8,6 +8,7 @@ struct MapView: View { let store: StoreOf @State private var shopImages: [String: UIImage] = [:] @State private var lastRegion: MKCoordinateRegion? + @Environment(\.dismiss) private var dismiss var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in @@ -55,6 +56,47 @@ struct MapView: View { .padding(.bottom, 30) } } + .navigationBarHidden(true) + .background( + NavigationLink( + destination: SearchView( + store: Store( + initialState: SearchStore.State(), + reducer: { SearchStore() } + ) + ), + isActive: viewStore.binding( + get: { $0.navigationPath.contains { $0 == .search } }, + send: { _ in .pop } + ) + ) { EmptyView() } + ) + .background( + NavigationLink( + destination: ShopDetailView( + store: Store( + initialState: ShopDetailStore.State( + shop: viewStore.selectedShop ?? ShopModel( + id: "", + name: "", + address: "", + latitude: 0, + longitude: 0, + imageUrl: "", + access: "", + openingHours: "", + genreCode: "" + ) + ), + reducer: { ShopDetailStore() } + ) + ), + isActive: viewStore.binding( + get: { $0.navigationPath.contains { if case .shopDetail = $0 { return true } else { return false } } }, + send: { _ in .pop } + ) + ) { EmptyView() } + ) } } diff --git a/HotSpot/Sources/Presentation/Search/Component/SearchFilterView.swift b/HotSpot/Sources/Presentation/Search/Component/SearchFilterView.swift index 23ab5be..cf0c501 100644 --- a/HotSpot/Sources/Presentation/Search/Component/SearchFilterView.swift +++ b/HotSpot/Sources/Presentation/Search/Component/SearchFilterView.swift @@ -3,14 +3,12 @@ import ComposableArchitecture struct SearchFilterView: View { let store: StoreOf - let coordinatorStore: StoreOf + @Environment(\.dismiss) private var dismiss var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in - WithViewStore(coordinatorStore, observe: { $0 }) { coordinatorViewStore in - NavigationView { - FilterForm(viewStore: viewStore, coordinatorViewStore: coordinatorViewStore) - } + NavigationView { + FilterForm(viewStore: viewStore) } } } @@ -18,7 +16,7 @@ struct SearchFilterView: View { private struct FilterForm: View { let viewStore: ViewStore - let coordinatorViewStore: ViewStore + @Environment(\.dismiss) private var dismiss var body: some View { Form { @@ -38,7 +36,7 @@ private struct FilterForm: View { ToolbarItem(placement: .navigationBarTrailing) { Button("Apply") { viewStore.send(.search(viewStore.searchText)) - viewStore.send(.toggleFilterSheet) + dismiss() } } } @@ -132,10 +130,6 @@ private struct DistanceSection: View { store: Store( initialState: SearchStore.State(), reducer: { SearchStore() } - ), - coordinatorStore: Store( - initialState: AppCoordinator.State(), - reducer: { AppCoordinator() } ) ) } \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index 6d3b812..f18be36 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -15,6 +15,7 @@ struct SearchStore { var selectedShop: ShopModel? = nil var paginationState: PaginationState = .init() var isFilterSheetPresented: Bool = false + var navigationPath: [NavigationDestination] = [] // Filter states var selectedBudget: Int = 0 @@ -42,10 +43,15 @@ struct SearchStore { lhs.isNonSmoking == rhs.isNonSmoking && lhs.hasParking == rhs.hasParking && lhs.selectedCuisine == rhs.selectedCuisine && - lhs.selectedDistance == rhs.selectedDistance + lhs.selectedDistance == rhs.selectedDistance && + lhs.navigationPath == rhs.navigationPath } } + enum NavigationDestination: Equatable { + case shopDetail(ShopModel) + } + enum Action { case onAppear case search(String) @@ -93,9 +99,13 @@ struct SearchStore { case let .selectShop(shop): state.selectedShop = shop + state.navigationPath.append(.shopDetail(shop)) return .none case .pop: + if !state.navigationPath.isEmpty { + state.navigationPath.removeLast() + } return .none case let .search(text): diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index 226103a..4925257 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -1,64 +1,87 @@ import SwiftUI import CoreLocation - import CobyDS import ComposableArchitecture struct SearchView: View { let store: StoreOf - let coordinatorStore: StoreOf @State private var isSearchFocused = false + @Environment(\.dismiss) private var dismiss var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in - WithViewStore(coordinatorStore, observe: { $0 }) { coordinatorViewStore in - VStack(spacing: 0) { - if !isSearchFocused { - TopBarView( - leftSide: .left, - leftAction: { - viewStore.send(.pop) - }, - rightSide: .icon, - rightIcon: UIImage.icMore, - rightAction: { - viewStore.send(.toggleFilterSheet) - } - ) - } - - SearchBar( - searchText: viewStore.searchText, - onSearch: { viewStore.send(.search($0)) }, - isSearchFocused: $isSearchFocused - ) - - SearchResults( - error: viewStore.error, - searchText: viewStore.searchText, - shops: viewStore.shops, - onSelectShop: { viewStore.send(.selectShop($0)) }, - onLoadMore: { - if !viewStore.paginationState.isLastPage { - viewStore.send(.loadMore) - } + VStack(spacing: 0) { + if !isSearchFocused { + TopBarView( + leftSide: .left, + leftAction: { + dismiss() + }, + rightSide: .icon, + rightIcon: UIImage.icMore, + rightAction: { + viewStore.send(.toggleFilterSheet) } ) } - .navigationBarHidden(true) - .onAppear { - viewStore.send(.onAppear) - } - .onTapGesture { - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) - } - .sheet(isPresented: viewStore.binding( - get: \.isFilterSheetPresented, - send: SearchStore.Action.toggleFilterSheet - )) { - SearchFilterView(store: store, coordinatorStore: coordinatorStore) - } + + SearchBar( + searchText: viewStore.searchText, + onSearch: { viewStore.send(.search($0)) }, + isSearchFocused: $isSearchFocused + ) + + SearchResults( + error: viewStore.error, + searchText: viewStore.searchText, + shops: viewStore.shops, + onSelectShop: { viewStore.send(.selectShop($0)) }, + onLoadMore: { + if !viewStore.paginationState.isLastPage { + viewStore.send(.loadMore) + } + } + ) + } + .navigationBarHidden(true) + .onAppear { + viewStore.send(.onAppear) + } + .onTapGesture { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } + .sheet(isPresented: viewStore.binding( + get: \.isFilterSheetPresented, + send: SearchStore.Action.toggleFilterSheet + )) { + SearchFilterView(store: store) + } + .background( + NavigationLink( + destination: ShopDetailView( + store: Store( + initialState: ShopDetailStore.State( + shop: viewStore.selectedShop ?? ShopModel( + id: "", + name: "", + address: "", + latitude: 0, + longitude: 0, + imageUrl: "", + access: "", + openingHours: "", + genreCode: "" + ) + ), + reducer: { ShopDetailStore() } + ) + ), + isActive: viewStore.binding( + get: { $0.navigationPath.contains { if case .shopDetail = $0 { return true } else { return false } } }, + send: { _ in .pop } + ) + ) { EmptyView() } + ) } } } @@ -68,10 +91,6 @@ struct SearchView: View { store: Store( initialState: SearchStore.State(), reducer: { SearchStore() } - ), - coordinatorStore: Store( - initialState: AppCoordinator.State(), - reducer: { AppCoordinator() } ) ) } diff --git a/HotSpot/Sources/Presentation/ShopDetail/Components/ShopImageSection.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopImageSection.swift similarity index 100% rename from HotSpot/Sources/Presentation/ShopDetail/Components/ShopImageSection.swift rename to HotSpot/Sources/Presentation/ShopDetail/Component/ShopImageSection.swift diff --git a/HotSpot/Sources/Presentation/ShopDetail/Components/ShopInfoSection.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift similarity index 100% rename from HotSpot/Sources/Presentation/ShopDetail/Components/ShopInfoSection.swift rename to HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift diff --git a/HotSpot/Sources/Presentation/ShopDetail/Components/ShopLocationMapView.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift similarity index 100% rename from HotSpot/Sources/Presentation/ShopDetail/Components/ShopLocationMapView.swift rename to HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift diff --git a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailStore.swift b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailStore.swift index 6e217d7..2b8cb87 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailStore.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailStore.swift @@ -1,22 +1,19 @@ import Foundation import ComposableArchitecture -@Reducer -struct ShopDetailStore { - @Dependency(\.shopRepository) var shopRepository - +struct ShopDetailStore: Reducer { struct State: Equatable { let shop: ShopModel } - - enum Action { - case pop + + enum Action: Equatable { + case onAppear } - + var body: some ReducerOf { Reduce { state, action in switch action { - case .pop: + case .onAppear: return .none } } diff --git a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift index bd30bd3..feac623 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift @@ -5,6 +5,7 @@ import Kingfisher struct ShopDetailView: View { let store: StoreOf + @Environment(\.dismiss) private var dismiss var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in @@ -13,7 +14,7 @@ struct ShopDetailView: View { TopBarView( leftSide: .left, leftAction: { - viewStore.send(.pop) + dismiss() } ) From 5bff11c98a147c6a2f8ac3b374b610cdb8b299b5 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 08:25:02 +0900 Subject: [PATCH 2/5] [FEAT] add custom navi --- HotSpot/Sources/App/AppDelegate.swift | 9 ++++++ .../App/CustomNavigationController.swift | 14 +++++++++ HotSpot/Sources/App/HotSpotApp.swift | 14 +++------ HotSpot/Sources/App/NavigationConfig.swift | 23 ++++++++++++++ HotSpot/Sources/App/SceneDelegate.swift | 30 +++++++++++++++++++ .../Sources/Presentation/Map/MapView.swift | 1 - .../Presentation/Search/SearchView.swift | 1 - .../ShopDetail/ShopDetailView.swift | 1 - 8 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 HotSpot/Sources/App/AppDelegate.swift create mode 100644 HotSpot/Sources/App/CustomNavigationController.swift create mode 100644 HotSpot/Sources/App/NavigationConfig.swift create mode 100644 HotSpot/Sources/App/SceneDelegate.swift diff --git a/HotSpot/Sources/App/AppDelegate.swift b/HotSpot/Sources/App/AppDelegate.swift new file mode 100644 index 0000000..cc87a67 --- /dev/null +++ b/HotSpot/Sources/App/AppDelegate.swift @@ -0,0 +1,9 @@ +import UIKit + +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) + sceneConfig.delegateClass = SceneDelegate.self + return sceneConfig + } +} \ No newline at end of file diff --git a/HotSpot/Sources/App/CustomNavigationController.swift b/HotSpot/Sources/App/CustomNavigationController.swift new file mode 100644 index 0000000..13367f5 --- /dev/null +++ b/HotSpot/Sources/App/CustomNavigationController.swift @@ -0,0 +1,14 @@ +import UIKit + +class CustomNavigationController: UINavigationController, UIGestureRecognizerDelegate { + override func viewDidLoad() { + super.viewDidLoad() + interactivePopGestureRecognizer?.delegate = self + interactivePopGestureRecognizer?.isEnabled = true + isNavigationBarHidden = true + } + + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return viewControllers.count > 1 + } +} \ No newline at end of file diff --git a/HotSpot/Sources/App/HotSpotApp.swift b/HotSpot/Sources/App/HotSpotApp.swift index 298f34f..214f064 100644 --- a/HotSpot/Sources/App/HotSpotApp.swift +++ b/HotSpot/Sources/App/HotSpotApp.swift @@ -1,18 +1,12 @@ import SwiftUI -import ComposableArchitecture @main struct HotSpotApp: App { + @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate + var body: some Scene { WindowGroup { - NavigationView { - MapView( - store: Store( - initialState: MapStore.State(), - reducer: { MapStore() } - ) - ) - } + Color.clear } } -} \ No newline at end of file +} diff --git a/HotSpot/Sources/App/NavigationConfig.swift b/HotSpot/Sources/App/NavigationConfig.swift new file mode 100644 index 0000000..76d2a1f --- /dev/null +++ b/HotSpot/Sources/App/NavigationConfig.swift @@ -0,0 +1,23 @@ +import SwiftUI +import UIKit + +extension UINavigationBar { + static func configureAppearance() { + let appearance = UINavigationBarAppearance() + appearance.configureWithTransparentBackground() + UINavigationBar.appearance().standardAppearance = appearance + UINavigationBar.appearance().scrollEdgeAppearance = appearance + UINavigationBar.appearance().isTranslucent = true + } +} + +struct NavigationControllerKey: EnvironmentKey { + static let defaultValue: UINavigationController = UINavigationController() +} + +extension EnvironmentValues { + var navigationController: UINavigationController { + get { self[NavigationControllerKey.self] } + set { self[NavigationControllerKey.self] = newValue } + } +} \ No newline at end of file diff --git a/HotSpot/Sources/App/SceneDelegate.swift b/HotSpot/Sources/App/SceneDelegate.swift new file mode 100644 index 0000000..42397bb --- /dev/null +++ b/HotSpot/Sources/App/SceneDelegate.swift @@ -0,0 +1,30 @@ +import SwiftUI +import UIKit +import ComposableArchitecture + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + + UINavigationBar.configureAppearance() + + let window = UIWindow(windowScene: windowScene) + let navigationController = CustomNavigationController() + + let mapView = MapView( + store: Store( + initialState: MapStore.State(), + reducer: { MapStore() } + ) + ) + + let hostingController = UIHostingController(rootView: mapView) + navigationController.viewControllers = [hostingController] + + window.rootViewController = navigationController + window.makeKeyAndVisible() + self.window = window + } +} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index d75e954..15341e2 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -56,7 +56,6 @@ struct MapView: View { .padding(.bottom, 30) } } - .navigationBarHidden(true) .background( NavigationLink( destination: SearchView( diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index 4925257..e76afd0 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -43,7 +43,6 @@ struct SearchView: View { } ) } - .navigationBarHidden(true) .onAppear { viewStore.send(.onAppear) } diff --git a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift index feac623..fdadafd 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift @@ -26,7 +26,6 @@ struct ShopDetailView: View { } } } - .navigationBarHidden(true) } } } From 774ff0961bb9bea95ff82198e174d13a58ed6c0a Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 08:36:31 +0900 Subject: [PATCH 3/5] [FEAT] add Coordinator --- .../App/Coordinator/AppCoordinator.swift | 67 ++++++++++++++++ HotSpot/Sources/App/NavigationConfig.swift | 11 +++ HotSpot/Sources/App/SceneDelegate.swift | 19 +---- .../Domain/Model/PaginationState.swift | 2 +- .../Sources/Presentation/Map/MapStore.swift | 50 +----------- .../Sources/Presentation/Map/MapView.swift | 46 +---------- .../Presentation/Search/SearchStore.swift | 77 ++++--------------- .../Presentation/Search/SearchView.swift | 32 +------- .../ShopDetail/ShopDetailView.swift | 4 +- 9 files changed, 108 insertions(+), 200 deletions(-) create mode 100644 HotSpot/Sources/App/Coordinator/AppCoordinator.swift diff --git a/HotSpot/Sources/App/Coordinator/AppCoordinator.swift b/HotSpot/Sources/App/Coordinator/AppCoordinator.swift new file mode 100644 index 0000000..06429af --- /dev/null +++ b/HotSpot/Sources/App/Coordinator/AppCoordinator.swift @@ -0,0 +1,67 @@ +import SwiftUI +import UIKit +import ComposableArchitecture + +final class AppCoordinator { + private let window: UIWindow + private let navigationController: UINavigationController + + init(window: UIWindow) { + self.window = window + self.navigationController = CustomNavigationController() + self.navigationController.isNavigationBarHidden = true + } + + func start() { + let mapView = MapView( + store: Store( + initialState: MapStore.State(), + reducer: { MapStore() } + ) + ) + .environment(\.coordinator, self) + + let hostingController = UIHostingController(rootView: mapView) + navigationController.viewControllers = [hostingController] + + window.rootViewController = navigationController + window.makeKeyAndVisible() + } + + func showSearch() { + let searchView = SearchView( + store: Store( + initialState: SearchStore.State(), + reducer: { SearchStore() } + ) + ) + .environment(\.coordinator, self) + + push(searchView) + } + + func showShopDetail(_ shop: ShopModel) { + let shopDetailView = ShopDetailView( + store: Store( + initialState: ShopDetailStore.State(shop: shop), + reducer: { ShopDetailStore() } + ) + ) + .environment(\.coordinator, self) + + push(shopDetailView) + } + + private func push(_ view: Content, animated: Bool = true) { + let hostingController = UIHostingController(rootView: view) + navigationController.pushViewController(hostingController, animated: animated) + } + + func pop(animated: Bool = true) { + navigationController.popViewController(animated: animated) + } + + func popToRoot(animated: Bool = true) { + navigationController.popToRootViewController(animated: animated) + } +} \ No newline at end of file diff --git a/HotSpot/Sources/App/NavigationConfig.swift b/HotSpot/Sources/App/NavigationConfig.swift index 76d2a1f..89e7067 100644 --- a/HotSpot/Sources/App/NavigationConfig.swift +++ b/HotSpot/Sources/App/NavigationConfig.swift @@ -20,4 +20,15 @@ extension EnvironmentValues { get { self[NavigationControllerKey.self] } set { self[NavigationControllerKey.self] = newValue } } +} + +struct CoordinatorKey: EnvironmentKey { + static let defaultValue: AppCoordinator? = nil +} + +extension EnvironmentValues { + var coordinator: AppCoordinator? { + get { self[CoordinatorKey.self] } + set { self[CoordinatorKey.self] = newValue } + } } \ No newline at end of file diff --git a/HotSpot/Sources/App/SceneDelegate.swift b/HotSpot/Sources/App/SceneDelegate.swift index 42397bb..8e330c0 100644 --- a/HotSpot/Sources/App/SceneDelegate.swift +++ b/HotSpot/Sources/App/SceneDelegate.swift @@ -1,9 +1,9 @@ import SwiftUI import UIKit -import ComposableArchitecture class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? + private var coordinator: AppCoordinator? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } @@ -11,20 +11,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { UINavigationBar.configureAppearance() let window = UIWindow(windowScene: windowScene) - let navigationController = CustomNavigationController() - - let mapView = MapView( - store: Store( - initialState: MapStore.State(), - reducer: { MapStore() } - ) - ) - - let hostingController = UIHostingController(rootView: mapView) - navigationController.viewControllers = [hostingController] - - window.rootViewController = navigationController - window.makeKeyAndVisible() self.window = window + + coordinator = AppCoordinator(window: window) + coordinator?.start() } } \ No newline at end of file diff --git a/HotSpot/Sources/Domain/Model/PaginationState.swift b/HotSpot/Sources/Domain/Model/PaginationState.swift index 2de256b..63ffbc4 100644 --- a/HotSpot/Sources/Domain/Model/PaginationState.swift +++ b/HotSpot/Sources/Domain/Model/PaginationState.swift @@ -1,6 +1,6 @@ import Foundation -struct PaginationState { +struct PaginationState: Equatable { var currentPage: Int var isLastPage: Bool var isLoading: Bool diff --git a/HotSpot/Sources/Presentation/Map/MapStore.swift b/HotSpot/Sources/Presentation/Map/MapStore.swift index 31e81c8..1f0734b 100644 --- a/HotSpot/Sources/Presentation/Map/MapStore.swift +++ b/HotSpot/Sources/Presentation/Map/MapStore.swift @@ -10,40 +10,12 @@ struct MapStore { struct State: Equatable { var shops: [ShopModel] = [] var visibleShops: [ShopModel] = [] - var selectedShop: ShopModel? = nil var region: MKCoordinateRegion = MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 35.6762, longitude: 139.6503), span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) ) var error: String? = nil var lastFetchedLocation: CLLocationCoordinate2D? = nil - var navigationPath: [NavigationDestination] = [] - } - - enum NavigationDestination: Hashable { - case search - case shopDetail(ShopModel) - - func hash(into hasher: inout Hasher) { - switch self { - case .search: - hasher.combine("search") - case .shopDetail(let shop): - hasher.combine("shopDetail") - hasher.combine(shop.id) - } - } - - static func == (lhs: NavigationDestination, rhs: NavigationDestination) -> Bool { - switch (lhs, rhs) { - case (.search, .search): - return true - case (.shopDetail(let lhsShop), .shopDetail(let rhsShop)): - return lhsShop.id == rhsShop.id - default: - return false - } - } } enum Action { @@ -51,9 +23,6 @@ struct MapStore { case fetchShops case updateShops([ShopModel]) case handleError(Error) - case showSearch - case showShopDetail(ShopModel) - case pop } var body: some ReducerOf { @@ -92,21 +61,6 @@ struct MapStore { case let .handleError(error): state.error = error.localizedDescription return .none - - case .showSearch: - state.navigationPath.append(.search) - return .none - - case .pop: - if !state.navigationPath.isEmpty { - state.navigationPath.removeLast() - } - return .none - - case let .showShopDetail(shop): - state.selectedShop = shop - state.navigationPath.append(.shopDetail(shop)) - return .none } } } @@ -144,14 +98,12 @@ extension MapStore.State { static func == (lhs: MapStore.State, rhs: MapStore.State) -> Bool { lhs.shops == rhs.shops && lhs.visibleShops == rhs.visibleShops && - lhs.selectedShop == rhs.selectedShop && lhs.region.center.latitude == rhs.region.center.latitude && lhs.region.center.longitude == rhs.region.center.longitude && lhs.region.span.latitudeDelta == rhs.region.span.latitudeDelta && lhs.region.span.longitudeDelta == rhs.region.span.longitudeDelta && lhs.error == rhs.error && lhs.lastFetchedLocation?.latitude == rhs.lastFetchedLocation?.latitude && - lhs.lastFetchedLocation?.longitude == rhs.lastFetchedLocation?.longitude && - lhs.navigationPath == rhs.navigationPath + lhs.lastFetchedLocation?.longitude == rhs.lastFetchedLocation?.longitude } } diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index 15341e2..1f000c3 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -8,7 +8,7 @@ struct MapView: View { let store: StoreOf @State private var shopImages: [String: UIImage] = [:] @State private var lastRegion: MKCoordinateRegion? - @Environment(\.dismiss) private var dismiss + @Environment(\.coordinator) private var coordinator var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in @@ -19,7 +19,7 @@ struct MapView: View { rightSide: .icon, rightIcon: UIImage.icSearch, rightAction: { - viewStore.send(.showSearch) + coordinator?.showSearch() } ) @@ -47,7 +47,7 @@ struct MapView: View { ) .frame(width: BaseSize.fullWidth) .onTapGesture { - viewStore.send(.showShopDetail(shop)) + coordinator?.showShopDetail(shop) } .onAppear { loadImage(for: shop) @@ -56,46 +56,6 @@ struct MapView: View { .padding(.bottom, 30) } } - .background( - NavigationLink( - destination: SearchView( - store: Store( - initialState: SearchStore.State(), - reducer: { SearchStore() } - ) - ), - isActive: viewStore.binding( - get: { $0.navigationPath.contains { $0 == .search } }, - send: { _ in .pop } - ) - ) { EmptyView() } - ) - .background( - NavigationLink( - destination: ShopDetailView( - store: Store( - initialState: ShopDetailStore.State( - shop: viewStore.selectedShop ?? ShopModel( - id: "", - name: "", - address: "", - latitude: 0, - longitude: 0, - imageUrl: "", - access: "", - openingHours: "", - genreCode: "" - ) - ), - reducer: { ShopDetailStore() } - ) - ), - isActive: viewStore.binding( - get: { $0.navigationPath.contains { if case .shopDetail = $0 { return true } else { return false } } }, - send: { _ in .pop } - ) - ) { EmptyView() } - ) } } diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index f18be36..b19a3ad 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -12,10 +12,8 @@ struct SearchStore { var searchText: String = "" var error: String? = nil var currentLocation: CLLocationCoordinate2D? - var selectedShop: ShopModel? = nil var paginationState: PaginationState = .init() var isFilterSheetPresented: Bool = false - var navigationPath: [NavigationDestination] = [] // Filter states var selectedBudget: Int = 0 @@ -32,10 +30,7 @@ struct SearchStore { lhs.error == rhs.error && lhs.currentLocation?.latitude == rhs.currentLocation?.latitude && lhs.currentLocation?.longitude == rhs.currentLocation?.longitude && - lhs.selectedShop == rhs.selectedShop && - lhs.paginationState.currentPage == rhs.paginationState.currentPage && - lhs.paginationState.isLastPage == rhs.paginationState.isLastPage && - lhs.paginationState.isLoading == rhs.paginationState.isLoading && + lhs.paginationState == rhs.paginationState && lhs.isFilterSheetPresented == rhs.isFilterSheetPresented && lhs.selectedBudget == rhs.selectedBudget && lhs.hasWiFi == rhs.hasWiFi && @@ -43,20 +38,13 @@ struct SearchStore { lhs.isNonSmoking == rhs.isNonSmoking && lhs.hasParking == rhs.hasParking && lhs.selectedCuisine == rhs.selectedCuisine && - lhs.selectedDistance == rhs.selectedDistance && - lhs.navigationPath == rhs.navigationPath + lhs.selectedDistance == rhs.selectedDistance } } - enum NavigationDestination: Equatable { - case shopDetail(ShopModel) - } - enum Action { case onAppear case search(String) - case selectShop(ShopModel) - case pop case updateLocation(CLLocationCoordinate2D) case updateShops([ShopModel]) case handleError(Error) @@ -97,23 +85,11 @@ struct SearchStore { state.error = error.localizedDescription return .none - case let .selectShop(shop): - state.selectedShop = shop - state.navigationPath.append(.shopDetail(shop)) - return .none - - case .pop: - if !state.navigationPath.isEmpty { - state.navigationPath.removeLast() - } - return .none - case let .search(text): guard text != state.searchText else { return .none } state.searchText = text state.paginationState.reset() - print("Search started - text: \(text)") return .run { [state] send in do { @@ -141,8 +117,6 @@ struct SearchStore { currentPage: 1 ) - print("Search result - currentPage: \(result.currentPage), hasMore: \(result.hasMore), shops count: \(result.shops.count)") - await send(.updateShops(result.shops)) await send(.updatePaginationState(PaginationState( currentPage: result.currentPage, @@ -153,16 +127,8 @@ struct SearchStore { await send(.handleError(error)) } } - + case .loadMore: - guard !state.paginationState.isLoading && !state.paginationState.isLastPage else { - print("LoadMore skipped - isLoading: \(state.paginationState.isLoading), isLastPage: \(state.paginationState.isLastPage), currentPage: \(state.paginationState.currentPage)") - return .none - } - - state.paginationState.startLoading() - print("Loading more - currentPage: \(state.paginationState.currentPage)") - return .run { [state] send in do { let location = state.currentLocation ?? CLLocationCoordinate2D(latitude: 34.6937, longitude: 135.5023) @@ -190,11 +156,7 @@ struct SearchStore { isLoadMore: true ) - print("LoadMore result - currentPage: \(result.currentPage), hasMore: \(result.hasMore), shops count: \(result.shops.count)") - - // Create a Set of existing shop IDs for quick lookup let existingShopIds = Set(state.shops.map { $0.id }) - // Filter out any shops that are already in the list let newShops = result.shops.filter { !existingShopIds.contains($0.id) } await send(.updateShops(state.shops + newShops)) @@ -205,52 +167,45 @@ struct SearchStore { ))) } catch { await send(.handleError(error)) - await send(.updatePaginationState(PaginationState( - currentPage: state.paginationState.currentPage, - isLastPage: state.paginationState.isLastPage, - isLoading: false - ))) } } - - case let .updatePaginationState(newState): - state.paginationState = newState - print("PaginationState updated - currentPage: \(newState.currentPage), isLastPage: \(newState.isLastPage), isLoading: \(newState.isLoading)") + + case let .updatePaginationState(paginationState): + state.paginationState = paginationState return .none - - // Filter actions + case .toggleFilterSheet: state.isFilterSheetPresented.toggle() return .none - + case let .updateBudget(budget): state.selectedBudget = budget return .none - + case .toggleWiFi: state.hasWiFi.toggle() return .none - + case .togglePrivateRoom: state.hasPrivateRoom.toggle() return .none - + case .toggleNonSmoking: state.isNonSmoking.toggle() return .none - + case .toggleParking: state.hasParking.toggle() return .none - + case let .updateCuisine(cuisine): state.selectedCuisine = cuisine return .none - + case let .updateDistance(distance): state.selectedDistance = distance return .none - + case .resetFilters: state.selectedBudget = 0 state.hasWiFi = false @@ -263,4 +218,4 @@ struct SearchStore { } } } -} +} diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index e76afd0..58efb47 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -6,7 +6,7 @@ import ComposableArchitecture struct SearchView: View { let store: StoreOf @State private var isSearchFocused = false - @Environment(\.dismiss) private var dismiss + @Environment(\.coordinator) private var coordinator var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in @@ -15,7 +15,7 @@ struct SearchView: View { TopBarView( leftSide: .left, leftAction: { - dismiss() + coordinator?.pop() }, rightSide: .icon, rightIcon: UIImage.icMore, @@ -35,7 +35,7 @@ struct SearchView: View { error: viewStore.error, searchText: viewStore.searchText, shops: viewStore.shops, - onSelectShop: { viewStore.send(.selectShop($0)) }, + onSelectShop: { coordinator?.showShopDetail($0) }, onLoadMore: { if !viewStore.paginationState.isLastPage { viewStore.send(.loadMore) @@ -55,32 +55,6 @@ struct SearchView: View { )) { SearchFilterView(store: store) } - .background( - NavigationLink( - destination: ShopDetailView( - store: Store( - initialState: ShopDetailStore.State( - shop: viewStore.selectedShop ?? ShopModel( - id: "", - name: "", - address: "", - latitude: 0, - longitude: 0, - imageUrl: "", - access: "", - openingHours: "", - genreCode: "" - ) - ), - reducer: { ShopDetailStore() } - ) - ), - isActive: viewStore.binding( - get: { $0.navigationPath.contains { if case .shopDetail = $0 { return true } else { return false } } }, - send: { _ in .pop } - ) - ) { EmptyView() } - ) } } } diff --git a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift index fdadafd..10a5946 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift @@ -5,7 +5,7 @@ import Kingfisher struct ShopDetailView: View { let store: StoreOf - @Environment(\.dismiss) private var dismiss + @Environment(\.coordinator) private var coordinator var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in @@ -14,7 +14,7 @@ struct ShopDetailView: View { TopBarView( leftSide: .left, leftAction: { - dismiss() + coordinator?.pop() } ) From 24810ebc8ac7c629a66c018b2855129ae7f4d287 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 08:56:03 +0900 Subject: [PATCH 4/5] [CHORE] move folder --- .../{App => Presentation}/Coordinator/AppCoordinator.swift | 3 +-- .../Coordinator}/CustomNavigationController.swift | 0 .../{App => Presentation/Coordinator}/NavigationConfig.swift | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) rename HotSpot/Sources/{App => Presentation}/Coordinator/AppCoordinator.swift (96%) rename HotSpot/Sources/{App => Presentation/Coordinator}/CustomNavigationController.swift (100%) rename HotSpot/Sources/{App => Presentation/Coordinator}/NavigationConfig.swift (99%) diff --git a/HotSpot/Sources/App/Coordinator/AppCoordinator.swift b/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift similarity index 96% rename from HotSpot/Sources/App/Coordinator/AppCoordinator.swift rename to HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift index 06429af..df795d0 100644 --- a/HotSpot/Sources/App/Coordinator/AppCoordinator.swift +++ b/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift @@ -9,7 +9,6 @@ final class AppCoordinator { init(window: UIWindow) { self.window = window self.navigationController = CustomNavigationController() - self.navigationController.isNavigationBarHidden = true } func start() { @@ -64,4 +63,4 @@ final class AppCoordinator { func popToRoot(animated: Bool = true) { navigationController.popToRootViewController(animated: animated) } -} \ No newline at end of file +} diff --git a/HotSpot/Sources/App/CustomNavigationController.swift b/HotSpot/Sources/Presentation/Coordinator/CustomNavigationController.swift similarity index 100% rename from HotSpot/Sources/App/CustomNavigationController.swift rename to HotSpot/Sources/Presentation/Coordinator/CustomNavigationController.swift diff --git a/HotSpot/Sources/App/NavigationConfig.swift b/HotSpot/Sources/Presentation/Coordinator/NavigationConfig.swift similarity index 99% rename from HotSpot/Sources/App/NavigationConfig.swift rename to HotSpot/Sources/Presentation/Coordinator/NavigationConfig.swift index 89e7067..d57c4b4 100644 --- a/HotSpot/Sources/App/NavigationConfig.swift +++ b/HotSpot/Sources/Presentation/Coordinator/NavigationConfig.swift @@ -31,4 +31,4 @@ extension EnvironmentValues { get { self[CoordinatorKey.self] } set { self[CoordinatorKey.self] = newValue } } -} \ No newline at end of file +} From bb2fa018f2e87b2b8bdf9b89636ec6dbb974363d Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 09:10:41 +0900 Subject: [PATCH 5/5] [FIX] fix search filter view --- .../Coordinator/AppCoordinator.swift | 12 ++ .../Sources/Presentation/Map/MapView.swift | 1 - .../Presentation/Search/SearchStore.swift | 103 ++++------------ .../Presentation/Search/SearchView.swift | 8 +- .../SearchFilter/SearchFilterStore.swift | 111 ++++++++++++++++++ .../SearchFilterView.swift | 34 +++--- 6 files changed, 162 insertions(+), 107 deletions(-) create mode 100644 HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift rename HotSpot/Sources/Presentation/{Search/Component => SearchFilter}/SearchFilterView.swift (75%) diff --git a/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift b/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift index df795d0..a6ec5ea 100644 --- a/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift +++ b/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift @@ -39,6 +39,18 @@ final class AppCoordinator { push(searchView) } + func showSearchFilter() { + let searchFilterView = SearchFilterView( + store: Store( + initialState: SearchFilterStore.State(), + reducer: { SearchFilterStore() } + ) + ) + .environment(\.coordinator, self) + + push(searchFilterView) + } + func showShopDetail(_ shop: ShopModel) { let shopDetailView = ShopDetailView( store: Store( diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index 1f000c3..893e5db 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -7,7 +7,6 @@ import Kingfisher struct MapView: View { let store: StoreOf @State private var shopImages: [String: UIImage] = [:] - @State private var lastRegion: MKCoordinateRegion? @Environment(\.coordinator) private var coordinator var body: some View { diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index b19a3ad..bb1e0d2 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -13,16 +13,8 @@ struct SearchStore { var error: String? = nil var currentLocation: CLLocationCoordinate2D? var paginationState: PaginationState = .init() - var isFilterSheetPresented: Bool = false - // Filter states - var selectedBudget: Int = 0 - var hasWiFi: Bool = false - var hasPrivateRoom: Bool = false - var isNonSmoking: Bool = false - var hasParking: Bool = false - var selectedCuisine: Int = 0 - var selectedDistance: Int = 3 + var filterState: SearchFilterStore.State = .init() static func == (lhs: State, rhs: State) -> Bool { lhs.shops == rhs.shops && @@ -31,14 +23,7 @@ struct SearchStore { lhs.currentLocation?.latitude == rhs.currentLocation?.latitude && lhs.currentLocation?.longitude == rhs.currentLocation?.longitude && lhs.paginationState == rhs.paginationState && - lhs.isFilterSheetPresented == rhs.isFilterSheetPresented && - lhs.selectedBudget == rhs.selectedBudget && - lhs.hasWiFi == rhs.hasWiFi && - lhs.hasPrivateRoom == rhs.hasPrivateRoom && - lhs.isNonSmoking == rhs.isNonSmoking && - lhs.hasParking == rhs.hasParking && - lhs.selectedCuisine == rhs.selectedCuisine && - lhs.selectedDistance == rhs.selectedDistance + lhs.filterState == rhs.filterState } } @@ -50,17 +35,7 @@ struct SearchStore { case handleError(Error) case loadMore case updatePaginationState(PaginationState) - - // Filter actions - case toggleFilterSheet - case updateBudget(Int) - case toggleWiFi - case togglePrivateRoom - case toggleNonSmoking - case toggleParking - case updateCuisine(Int) - case updateDistance(Int) - case resetFilters + case updateFilterState(SearchFilterStore.State) } var body: some ReducerOf { @@ -97,16 +72,16 @@ struct SearchStore { let request = ShopSearchRequestDTO( lat: location.latitude, lng: location.longitude, - range: state.selectedDistance, + range: state.filterState.selectedDistance, count: nil, keyword: text, - genre: state.selectedCuisine > 0 ? String(state.selectedCuisine) : nil, + genre: state.filterState.selectedCuisine > 0 ? String(state.filterState.selectedCuisine) : nil, order: nil, start: nil, - budget: state.selectedBudget > 0 ? String(state.selectedBudget) : nil, - privateRoom: state.hasPrivateRoom ? true : nil, - wifi: state.hasWiFi ? true : nil, - nonSmoking: state.isNonSmoking ? true : nil, + budget: state.filterState.selectedBudget > 0 ? String(state.filterState.selectedBudget) : nil, + privateRoom: state.filterState.hasPrivateRoom ? true : nil, + wifi: state.filterState.hasWiFi ? true : nil, + nonSmoking: state.filterState.isNonSmoking ? true : nil, coupon: nil, openNow: nil ) @@ -135,16 +110,16 @@ struct SearchStore { let request = ShopSearchRequestDTO( lat: location.latitude, lng: location.longitude, - range: state.selectedDistance, + range: state.filterState.selectedDistance, count: nil, keyword: state.searchText, - genre: state.selectedCuisine > 0 ? String(state.selectedCuisine) : nil, + genre: state.filterState.selectedCuisine > 0 ? String(state.filterState.selectedCuisine) : nil, order: nil, start: nil, - budget: state.selectedBudget > 0 ? String(state.selectedBudget) : nil, - privateRoom: state.hasPrivateRoom ? true : nil, - wifi: state.hasWiFi ? true : nil, - nonSmoking: state.isNonSmoking ? true : nil, + budget: state.filterState.selectedBudget > 0 ? String(state.filterState.selectedBudget) : nil, + privateRoom: state.filterState.hasPrivateRoom ? true : nil, + wifi: state.filterState.hasWiFi ? true : nil, + nonSmoking: state.filterState.isNonSmoking ? true : nil, coupon: nil, openNow: nil ) @@ -173,48 +148,12 @@ struct SearchStore { case let .updatePaginationState(paginationState): state.paginationState = paginationState return .none - - case .toggleFilterSheet: - state.isFilterSheetPresented.toggle() - return .none - - case let .updateBudget(budget): - state.selectedBudget = budget - return .none - - case .toggleWiFi: - state.hasWiFi.toggle() - return .none - - case .togglePrivateRoom: - state.hasPrivateRoom.toggle() - return .none - - case .toggleNonSmoking: - state.isNonSmoking.toggle() - return .none - - case .toggleParking: - state.hasParking.toggle() - return .none - - case let .updateCuisine(cuisine): - state.selectedCuisine = cuisine - return .none - - case let .updateDistance(distance): - state.selectedDistance = distance - return .none - - case .resetFilters: - state.selectedBudget = 0 - state.hasWiFi = false - state.hasPrivateRoom = false - state.isNonSmoking = false - state.hasParking = false - state.selectedCuisine = 0 - state.selectedDistance = 3 - return .none + + case let .updateFilterState(filterState): + state.filterState = filterState + return .run { [state] send in + await send(.search(state.searchText)) + } } } } diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index 58efb47..5123534 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -20,7 +20,7 @@ struct SearchView: View { rightSide: .icon, rightIcon: UIImage.icMore, rightAction: { - viewStore.send(.toggleFilterSheet) + coordinator?.showSearchFilter() } ) } @@ -49,12 +49,6 @@ struct SearchView: View { .onTapGesture { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } - .sheet(isPresented: viewStore.binding( - get: \.isFilterSheetPresented, - send: SearchStore.Action.toggleFilterSheet - )) { - SearchFilterView(store: store) - } } } } diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift new file mode 100644 index 0000000..01820f8 --- /dev/null +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift @@ -0,0 +1,111 @@ +import Foundation +import ComposableArchitecture + +@Reducer +struct SearchFilterStore { + private enum UserDefaultsKey { + static let budget = "shop_filter_budget" + static let hasWiFi = "shop_filter_has_wifi" + static let hasPrivateRoom = "shop_filter_has_private_room" + static let isNonSmoking = "shop_filter_is_non_smoking" + static let hasParking = "shop_filter_has_parking" + static let cuisine = "shop_filter_cuisine" + static let distance = "shop_filter_distance" + } + + struct State: Equatable { + var selectedBudget: Int + var hasWiFi: Bool + var hasPrivateRoom: Bool + var isNonSmoking: Bool + var hasParking: Bool + var selectedCuisine: Int + var selectedDistance: Int + + init() { + let defaults = UserDefaults.standard + self.selectedBudget = defaults.integer(forKey: UserDefaultsKey.budget) + self.hasWiFi = defaults.bool(forKey: UserDefaultsKey.hasWiFi) + self.hasPrivateRoom = defaults.bool(forKey: UserDefaultsKey.hasPrivateRoom) + self.isNonSmoking = defaults.bool(forKey: UserDefaultsKey.isNonSmoking) + self.hasParking = defaults.bool(forKey: UserDefaultsKey.hasParking) + self.selectedCuisine = defaults.integer(forKey: UserDefaultsKey.cuisine) + self.selectedDistance = defaults.integer(forKey: UserDefaultsKey.distance) + } + } + + enum Action { + case updateBudget(Int) + case toggleWiFi + case togglePrivateRoom + case toggleNonSmoking + case toggleParking + case updateCuisine(Int) + case updateDistance(Int) + case resetFilters + case applyFilters + } + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case let .updateBudget(budget): + state.selectedBudget = budget + UserDefaults.standard.set(budget, forKey: UserDefaultsKey.budget) + return .none + + case .toggleWiFi: + state.hasWiFi.toggle() + UserDefaults.standard.set(state.hasWiFi, forKey: UserDefaultsKey.hasWiFi) + return .none + + case .togglePrivateRoom: + state.hasPrivateRoom.toggle() + UserDefaults.standard.set(state.hasPrivateRoom, forKey: UserDefaultsKey.hasPrivateRoom) + return .none + + case .toggleNonSmoking: + state.isNonSmoking.toggle() + UserDefaults.standard.set(state.isNonSmoking, forKey: UserDefaultsKey.isNonSmoking) + return .none + + case .toggleParking: + state.hasParking.toggle() + UserDefaults.standard.set(state.hasParking, forKey: UserDefaultsKey.hasParking) + return .none + + case let .updateCuisine(cuisine): + state.selectedCuisine = cuisine + UserDefaults.standard.set(cuisine, forKey: UserDefaultsKey.cuisine) + return .none + + case let .updateDistance(distance): + state.selectedDistance = distance + UserDefaults.standard.set(distance, forKey: UserDefaultsKey.distance) + return .none + + case .resetFilters: + state.selectedBudget = 0 + state.hasWiFi = false + state.hasPrivateRoom = false + state.isNonSmoking = false + state.hasParking = false + state.selectedCuisine = 0 + state.selectedDistance = 3 + + UserDefaults.standard.removeObject(forKey: UserDefaultsKey.budget) + UserDefaults.standard.removeObject(forKey: UserDefaultsKey.hasWiFi) + UserDefaults.standard.removeObject(forKey: UserDefaultsKey.hasPrivateRoom) + UserDefaults.standard.removeObject(forKey: UserDefaultsKey.isNonSmoking) + UserDefaults.standard.removeObject(forKey: UserDefaultsKey.hasParking) + UserDefaults.standard.removeObject(forKey: UserDefaultsKey.cuisine) + UserDefaults.standard.removeObject(forKey: UserDefaultsKey.distance) + + return .none + + case .applyFilters: + return .none + } + } + } +} diff --git a/HotSpot/Sources/Presentation/Search/Component/SearchFilterView.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift similarity index 75% rename from HotSpot/Sources/Presentation/Search/Component/SearchFilterView.swift rename to HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift index cf0c501..5c356c3 100644 --- a/HotSpot/Sources/Presentation/Search/Component/SearchFilterView.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift @@ -2,7 +2,7 @@ import SwiftUI import ComposableArchitecture struct SearchFilterView: View { - let store: StoreOf + let store: StoreOf @Environment(\.dismiss) private var dismiss var body: some View { @@ -15,7 +15,7 @@ struct SearchFilterView: View { } private struct FilterForm: View { - let viewStore: ViewStore + let viewStore: ViewStore @Environment(\.dismiss) private var dismiss var body: some View { @@ -35,7 +35,7 @@ private struct FilterForm: View { } ToolbarItem(placement: .navigationBarTrailing) { Button("Apply") { - viewStore.send(.search(viewStore.searchText)) + viewStore.send(.applyFilters) dismiss() } } @@ -44,13 +44,13 @@ private struct FilterForm: View { } private struct BudgetSection: View { - let viewStore: ViewStore + let viewStore: ViewStore var body: some View { Section(header: Text("Budget")) { Picker("Budget", selection: viewStore.binding( get: \.selectedBudget, - send: SearchStore.Action.updateBudget + send: SearchFilterStore.Action.updateBudget )) { Text("Any").tag(0) Text("¥1,000~").tag(1) @@ -63,38 +63,38 @@ private struct BudgetSection: View { } private struct FeaturesSection: View { - let viewStore: ViewStore + let viewStore: ViewStore var body: some View { Section(header: Text("Features")) { Toggle("WiFi Available", isOn: viewStore.binding( get: \.hasWiFi, - send: SearchStore.Action.toggleWiFi + send: SearchFilterStore.Action.toggleWiFi )) Toggle("Private Room", isOn: viewStore.binding( get: \.hasPrivateRoom, - send: SearchStore.Action.togglePrivateRoom + send: SearchFilterStore.Action.togglePrivateRoom )) Toggle("Non-Smoking", isOn: viewStore.binding( get: \.isNonSmoking, - send: SearchStore.Action.toggleNonSmoking + send: SearchFilterStore.Action.toggleNonSmoking )) Toggle("Parking Available", isOn: viewStore.binding( get: \.hasParking, - send: SearchStore.Action.toggleParking + send: SearchFilterStore.Action.toggleParking )) } } } private struct CuisineSection: View { - let viewStore: ViewStore + let viewStore: ViewStore var body: some View { Section(header: Text("Cuisine")) { Picker("Cuisine", selection: viewStore.binding( get: \.selectedCuisine, - send: SearchStore.Action.updateCuisine + send: SearchFilterStore.Action.updateCuisine )) { Text("Any").tag(0) Text("Japanese").tag(1) @@ -107,13 +107,13 @@ private struct CuisineSection: View { } private struct DistanceSection: View { - let viewStore: ViewStore + let viewStore: ViewStore var body: some View { Section(header: Text("Distance")) { Picker("Distance", selection: viewStore.binding( get: \.selectedDistance, - send: SearchStore.Action.updateDistance + send: SearchFilterStore.Action.updateDistance )) { Text("300m").tag(1) Text("500m").tag(2) @@ -128,8 +128,8 @@ private struct DistanceSection: View { #Preview { SearchFilterView( store: Store( - initialState: SearchStore.State(), - reducer: { SearchStore() } + initialState: SearchFilterStore.State(), + reducer: { SearchFilterStore() } ) ) -} \ No newline at end of file +}