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/HotSpotApp.swift b/HotSpot/Sources/App/HotSpotApp.swift index 93a2769..214f064 100644 --- a/HotSpot/Sources/App/HotSpotApp.swift +++ b/HotSpot/Sources/App/HotSpotApp.swift @@ -1,16 +1,12 @@ import SwiftUI -import ComposableArchitecture @main struct HotSpotApp: App { + @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate + var body: some Scene { WindowGroup { - AppCoordinatorView( - store: Store( - initialState: AppCoordinator.State(), - reducer: { AppCoordinator() } - ) - ) + Color.clear } } -} \ 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..8e330c0 --- /dev/null +++ b/HotSpot/Sources/App/SceneDelegate.swift @@ -0,0 +1,19 @@ +import SwiftUI +import UIKit + +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 } + + UINavigationBar.configureAppearance() + + let window = UIWindow(windowScene: windowScene) + 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/Coordinator/AppCoordinator.swift b/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift index 5edf3c1..a6ec5ea 100644 --- a/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift +++ b/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift @@ -1,84 +1,78 @@ import SwiftUI +import UIKit 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 +final class AppCoordinator { + private let window: UIWindow + private let navigationController: UINavigationController + + init(window: UIWindow) { + self.window = window + self.navigationController = CustomNavigationController() } - - enum Action { - case map(MapStore.Action) - case search(SearchStore.Action) - case shopDetail(ShopDetailStore.Action) - - case showShopDetail(ShopModel) - case showSearch - case dismissSearch - case dismissDetail + + 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() } - - var body: some ReducerOf { - Scope(state: \.map, action: \.map) { - MapStore() - } + + func showSearch() { + let searchView = SearchView( + store: Store( + initialState: SearchStore.State(), + reducer: { SearchStore() } + ) + ) + .environment(\.coordinator, self) + + 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( + initialState: ShopDetailStore.State(shop: shop), + reducer: { ShopDetailStore() } + ) + ) + .environment(\.coordinator, self) - 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() - } + 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) } -} +} 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/Coordinator/CustomNavigationController.swift b/HotSpot/Sources/Presentation/Coordinator/CustomNavigationController.swift new file mode 100644 index 0000000..13367f5 --- /dev/null +++ b/HotSpot/Sources/Presentation/Coordinator/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/Presentation/Coordinator/NavigationConfig.swift b/HotSpot/Sources/Presentation/Coordinator/NavigationConfig.swift new file mode 100644 index 0000000..d57c4b4 --- /dev/null +++ b/HotSpot/Sources/Presentation/Coordinator/NavigationConfig.swift @@ -0,0 +1,34 @@ +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 } + } +} + +struct CoordinatorKey: EnvironmentKey { + static let defaultValue: AppCoordinator? = nil +} + +extension EnvironmentValues { + var coordinator: AppCoordinator? { + get { self[CoordinatorKey.self] } + set { self[CoordinatorKey.self] = newValue } + } +} diff --git a/HotSpot/Sources/Presentation/Map/MapStore.swift b/HotSpot/Sources/Presentation/Map/MapStore.swift index 832661f..1f0734b 100644 --- a/HotSpot/Sources/Presentation/Map/MapStore.swift +++ b/HotSpot/Sources/Presentation/Map/MapStore.swift @@ -10,7 +10,6 @@ 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) @@ -24,8 +23,6 @@ struct MapStore { case fetchShops case updateShops([ShopModel]) case handleError(Error) - case showSearch - case showShopDetail(ShopModel) } var body: some ReducerOf { @@ -64,13 +61,6 @@ struct MapStore { case let .handleError(error): state.error = error.localizedDescription return .none - - case .showSearch: - return .none - - case let .showShopDetail(shop): - state.selectedShop = shop - return .none } } } @@ -108,7 +98,6 @@ 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 && diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index 5879f25..893e5db 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -7,7 +7,7 @@ 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 { WithViewStore(store, observe: { $0 }) { viewStore in @@ -18,7 +18,7 @@ struct MapView: View { rightSide: .icon, rightIcon: UIImage.icSearch, rightAction: { - viewStore.send(.showSearch) + coordinator?.showSearch() } ) @@ -46,7 +46,7 @@ struct MapView: View { ) .frame(width: BaseSize.fullWidth) .onTapGesture { - viewStore.send(.showShopDetail(shop)) + coordinator?.showShopDetail(shop) } .onAppear { loadImage(for: shop) diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index 6d3b812..bb1e0d2 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -12,18 +12,9 @@ struct SearchStore { var searchText: String = "" var error: String? = nil var currentLocation: CLLocationCoordinate2D? - var selectedShop: ShopModel? = nil 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,42 +22,20 @@ 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.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.paginationState == rhs.paginationState && + lhs.filterState == rhs.filterState } } enum Action { case onAppear case search(String) - case selectShop(ShopModel) - case pop case updateLocation(CLLocationCoordinate2D) case updateShops([ShopModel]) 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 { @@ -91,19 +60,11 @@ struct SearchStore { state.error = error.localizedDescription return .none - case let .selectShop(shop): - state.selectedShop = shop - return .none - - case .pop: - 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 { @@ -111,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 ) @@ -131,8 +92,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, @@ -143,32 +102,24 @@ 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) 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 ) @@ -180,11 +131,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)) @@ -195,62 +142,19 @@ 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)") - 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 + + case let .updatePaginationState(paginationState): + state.paginationState = paginationState 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 226103a..5123534 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -1,63 +1,53 @@ import SwiftUI import CoreLocation - import CobyDS import ComposableArchitecture struct SearchView: View { let store: StoreOf - let coordinatorStore: StoreOf @State private var isSearchFocused = false + @Environment(\.coordinator) private var coordinator 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: { + coordinator?.pop() + }, + rightSide: .icon, + rightIcon: UIImage.icMore, + rightAction: { + coordinator?.showSearchFilter() } ) } - .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: { coordinator?.showShopDetail($0) }, + onLoadMore: { + if !viewStore.paginationState.isLastPage { + viewStore.send(.loadMore) + } + } + ) + } + .onAppear { + viewStore.send(.onAppear) + } + .onTapGesture { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } } @@ -68,10 +58,6 @@ struct SearchView: View { store: Store( initialState: SearchStore.State(), reducer: { SearchStore() } - ), - coordinatorStore: Store( - initialState: AppCoordinator.State(), - reducer: { AppCoordinator() } ) ) } 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 66% rename from HotSpot/Sources/Presentation/Search/Component/SearchFilterView.swift rename to HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift index 23ab5be..5c356c3 100644 --- a/HotSpot/Sources/Presentation/Search/Component/SearchFilterView.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift @@ -2,23 +2,21 @@ import SwiftUI import ComposableArchitecture struct SearchFilterView: View { - let store: StoreOf - let coordinatorStore: StoreOf + let store: 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) } } } } private struct FilterForm: View { - let viewStore: ViewStore - let coordinatorViewStore: ViewStore + let viewStore: ViewStore + @Environment(\.dismiss) private var dismiss var body: some View { Form { @@ -37,8 +35,8 @@ private struct FilterForm: View { } ToolbarItem(placement: .navigationBarTrailing) { Button("Apply") { - viewStore.send(.search(viewStore.searchText)) - viewStore.send(.toggleFilterSheet) + viewStore.send(.applyFilters) + dismiss() } } } @@ -46,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) @@ -65,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) @@ -109,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) @@ -130,12 +128,8 @@ private struct DistanceSection: View { #Preview { SearchFilterView( store: Store( - initialState: SearchStore.State(), - reducer: { SearchStore() } - ), - coordinatorStore: Store( - initialState: AppCoordinator.State(), - reducer: { AppCoordinator() } + initialState: SearchFilterStore.State(), + reducer: { SearchFilterStore() } ) ) -} \ No newline at end of file +} 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..10a5946 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(\.coordinator) private var coordinator var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in @@ -13,7 +14,7 @@ struct ShopDetailView: View { TopBarView( leftSide: .left, leftAction: { - viewStore.send(.pop) + coordinator?.pop() } ) @@ -25,7 +26,6 @@ struct ShopDetailView: View { } } } - .navigationBarHidden(true) } } }