diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index fce070fa..09610d26 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -2479,7 +2479,7 @@ repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture.git"; requirement = { kind = exactVersion; - version = 1.5.6; + version = 1.7.3; }; }; ABAC82FC26BC4866009F5026 /* XCRemoteSwiftPackageReference "SwiftyOpenCC" */ = { diff --git a/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c8c973cc..8ab11e4d 100644 --- a/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", "state" : { - "revision" : "3568f01377c6c668aad40d066acf97ce670a1dad", - "version" : "1.5.6" + "revision" : "f815e76b520aacfad4ff35c7e1f036f8add6f4b4", + "version" : "1.7.3" } }, { @@ -145,6 +145,15 @@ "version" : "1.1.0" } }, + { + "identity" : "swift-perception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-perception", + "state" : { + "revision" : "d3ab98dc2887d1cc3bed676f6fa354da4cb22b3c", + "version" : "1.2.4" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", diff --git a/EhPanda/App/EhPandaApp.swift b/EhPanda/App/EhPandaApp.swift index bc19879c..4aa69ab1 100644 --- a/EhPanda/App/EhPandaApp.swift +++ b/EhPanda/App/EhPandaApp.swift @@ -13,24 +13,22 @@ import ComposableArchitecture var body: some Scene { WindowGroup { - WithViewStore( - appDelegate.store, observe: \.appDelegateState.migrationState.databaseState - ) { viewStore in - ZStack { - if viewStore.state == .idle { - TabBarView(store: appDelegate.store).onAppear(perform: addTouchHandler).accentColor(.primary) - } - MigrationView( - store: appDelegate.store.scope( - state: \.appDelegateState.migrationState, - action: \.appDelegate.migration - ) - ) - .opacity(viewStore.state != .idle ? 1 : 0) - .animation(.linear(duration: 0.5), value: viewStore.state) + ZStack { + let databaseState = appDelegate.store.appDelegateState.migrationState.databaseState + + if databaseState == .idle { + TabBarView(store: appDelegate.store).onAppear(perform: addTouchHandler).accentColor(.primary) } - .navigationViewStyle(.stack) + MigrationView( + store: appDelegate.store.scope( + state: \.appDelegateState.migrationState, + action: \.appDelegate.migration + ) + ) + .opacity(databaseState != .idle ? 1 : 0) + .animation(.linear(duration: 0.5), value: databaseState) } + .navigationViewStyle(.stack) } } } diff --git a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift index fb6e445c..e09111c3 100644 --- a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift +++ b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift @@ -67,11 +67,7 @@ where State == Base.State, Action == Base.Action { @ReducerBuilder var body: some Reducer { Reduce { state, action in - if case .setting(.binding(let bindingAction)) = action as? AppReducer.Action { - Logger.info("setting(EhPanda.SettingReducer.Action.\(bindingAction.customDumpDescription)") - } else { - Logger.info(action) - } + Logger.info(action) return base.reduce(into: &state, action: action) } } diff --git a/EhPanda/DataFlow/AppDelegateReducer.swift b/EhPanda/DataFlow/AppDelegateReducer.swift index 39310d20..7142bde8 100644 --- a/EhPanda/DataFlow/AppDelegateReducer.swift +++ b/EhPanda/DataFlow/AppDelegateReducer.swift @@ -11,6 +11,7 @@ import ComposableArchitecture @Reducer struct AppDelegateReducer { + @ObservableState struct State: Equatable { var migrationState = MigrationReducer.State() } @@ -57,7 +58,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let store = Store(initialState: .init()) { AppReducer() } - lazy var viewStore = ViewStore(store, observe: { $0 }) static var orientationMask: UIInterfaceOrientationMask = DeviceUtil.isPad ? .all : [.portrait, .portraitUpsideDown] @@ -70,7 +70,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { if !AppUtil.isTesting { - viewStore.send(.appDelegate(.onLaunchFinish)) + store.send(.appDelegate(.onLaunchFinish)) } return true } diff --git a/EhPanda/DataFlow/AppLockReducer.swift b/EhPanda/DataFlow/AppLockReducer.swift index e2060ab3..6a3cf513 100644 --- a/EhPanda/DataFlow/AppLockReducer.swift +++ b/EhPanda/DataFlow/AppLockReducer.swift @@ -10,8 +10,9 @@ import ComposableArchitecture @Reducer struct AppLockReducer { + @ObservableState struct State: Equatable { - @BindingState var blurRadius: Double = 0 + var blurRadius: Double = 0 var becameInactiveDate: Date? var isAppLocked = false diff --git a/EhPanda/DataFlow/AppReducer.swift b/EhPanda/DataFlow/AppReducer.swift index 784a3af3..5dcc39dc 100644 --- a/EhPanda/DataFlow/AppReducer.swift +++ b/EhPanda/DataFlow/AppReducer.swift @@ -10,15 +10,16 @@ import ComposableArchitecture @Reducer struct AppReducer { + @ObservableState struct State: Equatable { var appDelegateState = AppDelegateReducer.State() - @BindingState var appRouteState = AppRouteReducer.State() + var appRouteState = AppRouteReducer.State() var appLockState = AppLockReducer.State() var tabBarState = TabBarReducer.State() var homeState = HomeReducer.State() var favoritesState = FavoritesReducer.State() var searchRootState = SearchRootReducer.State() - @BindingState var settingState = SettingReducer.State() + var settingState = SettingReducer.State() } enum Action: BindableAction { @@ -45,14 +46,10 @@ struct AppReducer { LoggingReducer { BindingReducer() .onChange(of: \.appRouteState.route) { _, newValue in - Reduce { _, _ in - return newValue == nil ? .send(.appRoute(.clearSubStates)) : .none - } + Reduce({ _, _ in newValue == nil ? .send(.appRoute(.clearSubStates)) : .none }) } .onChange(of: \.settingState.setting) { _, _ in - Reduce { _, _ in - return .send(.setting(.syncSetting)) - } + Reduce({ _, _ in .send(.setting(.syncSetting)) }) } Reduce { state, action in diff --git a/EhPanda/DataFlow/AppRouteReducer.swift b/EhPanda/DataFlow/AppRouteReducer.swift index 0cf0c40f..20ed7f8d 100644 --- a/EhPanda/DataFlow/AppRouteReducer.swift +++ b/EhPanda/DataFlow/AppRouteReducer.swift @@ -19,14 +19,15 @@ struct AppRouteReducer { case newDawn(Greeting) } + @ObservableState struct State: Equatable { - @BindingState var route: Route? + var route: Route? var hudConfig: TTProgressHUDConfig = .loading - @Heap var detailState: DetailReducer.State! + var detailState: Heap init() { - _detailState = .init(.init()) + detailState = .init(.init()) } } @@ -57,12 +58,12 @@ struct AppRouteReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - case .binding: return .none @@ -75,7 +76,7 @@ struct AppRouteReducer { return .none case .clearSubStates: - state.detailState = .init() + state.detailState.wrappedValue = .init() return .send(.detail(.teardown)) case .detectClipboardURL: @@ -97,7 +98,7 @@ struct AppRouteReducer { if case .detail = state.route { delay = 1000 state.route = nil - state.detailState = .init() + state.detailState.wrappedValue = .init() } let (isGalleryImageURL, _, _) = urlClient.analyzeURL(url) let gid = urlClient.parseGalleryID(url) @@ -116,7 +117,7 @@ struct AppRouteReducer { let (_, pageIndex, commentID) = urlClient.analyzeURL(url) let gid = urlClient.parseGalleryID(url) var effects = [Effect]() - state.detailState = .init() + state.detailState.wrappedValue = .init() effects.append(.send(.detail(.fetchDatabaseInfos(gid)))) if let pageIndex = pageIndex { effects.append(.send(.updateReadingProgress(gid, pageIndex))) @@ -127,7 +128,7 @@ struct AppRouteReducer { } ) } else if let commentID = commentID { - state.detailState.commentsState?.scrollCommentID = commentID + state.detailState.wrappedValue?.commentsState.wrappedValue?.scrollCommentID = commentID effects.append( .run { send in try await Task.sleep(for: .milliseconds(500)) @@ -190,6 +191,6 @@ struct AppRouteReducer { hapticsClient: hapticsClient ) - Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Detail/Archives/ArchivesReducer.swift b/EhPanda/View/Detail/Archives/ArchivesReducer.swift index 4197cee8..e9acdd37 100644 --- a/EhPanda/View/Detail/Archives/ArchivesReducer.swift +++ b/EhPanda/View/Detail/Archives/ArchivesReducer.swift @@ -20,9 +20,10 @@ struct ArchivesReducer { case fetchArchive, fetchArchiveFunds, fetchDownloadResponse } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var selectedArchive: GalleryArchive.HathArchive? + var route: Route? + var selectedArchive: GalleryArchive.HathArchive? var loadingState: LoadingState = .idle var hathArchives = [GalleryArchive.HathArchive]() diff --git a/EhPanda/View/Detail/Archives/ArchivesView.swift b/EhPanda/View/Detail/Archives/ArchivesView.swift index 6ba83b28..f8b5388b 100644 --- a/EhPanda/View/Detail/Archives/ArchivesView.swift +++ b/EhPanda/View/Detail/Archives/ArchivesView.swift @@ -9,8 +9,7 @@ import SwiftUI import ComposableArchitecture struct ArchivesView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let gid: String private let user: User private let galleryURL: URL @@ -21,7 +20,6 @@ struct ArchivesView: View { gid: String, user: User, galleryURL: URL, archiveURL: URL ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.gid = gid self.user = user self.galleryURL = galleryURL @@ -33,41 +31,41 @@ struct ArchivesView: View { NavigationView { ZStack { VStack { - HathArchivesView(archives: viewStore.hathArchives, selection: viewStore.$selectedArchive) + HathArchivesView(archives: store.hathArchives, selection: $store.selectedArchive) Spacer() if let credits = Int(user.credits ?? ""), let galleryPoints = Int(user.galleryPoints ?? "") { ArchiveFundsView(credits: credits, galleryPoints: galleryPoints) } - DownloadButton(isDisabled: viewStore.selectedArchive == nil) { - viewStore.send(.fetchDownloadResponse(archiveURL)) + DownloadButton(isDisabled: store.selectedArchive == nil) { + store.send(.fetchDownloadResponse(archiveURL)) } } - .padding(.horizontal).opacity(viewStore.hathArchives.isEmpty ? 0 : 1) + .padding(.horizontal).opacity(store.hathArchives.isEmpty ? 0 : 1) LoadingView().opacity( - viewStore.loadingState == .loading - && viewStore.hathArchives.isEmpty ? 1 : 0 + store.loadingState == .loading + && store.hathArchives.isEmpty ? 1 : 0 ) - let error = (/LoadingState.failed).extract(from: viewStore.loadingState) + let error = (/LoadingState.failed).extract(from: store.loadingState) ErrorView(error: error ?? .unknown) { - viewStore.send(.fetchArchive(gid, galleryURL, archiveURL)) + store.send(.fetchArchive(gid, galleryURL, archiveURL)) } - .opacity(error != nil && viewStore.hathArchives.isEmpty ? 1 : 0) + .opacity(error != nil && store.hathArchives.isEmpty ? 1 : 0) } .progressHUD( - config: viewStore.communicatingHUDConfig, - unwrapping: viewStore.$route, + config: store.communicatingHUDConfig, + unwrapping: $store.route, case: /ArchivesReducer.Route.communicatingHUD ) .progressHUD( - config: viewStore.messageHUDConfig, - unwrapping: viewStore.$route, + config: store.messageHUDConfig, + unwrapping: $store.route, case: /ArchivesReducer.Route.messageHUD ) - .animation(.default, value: viewStore.hathArchives) + .animation(.default, value: store.hathArchives) .animation(.default, value: user.galleryPoints) .animation(.default, value: user.credits) .onAppear { - viewStore.send(.fetchArchive(gid, galleryURL, archiveURL)) + store.send(.fetchArchive(gid, galleryURL, archiveURL)) } .navigationTitle(L10n.Localizable.ArchivesView.Title.archives) } diff --git a/EhPanda/View/Detail/Comments/CommentsReducer.swift b/EhPanda/View/Detail/Comments/CommentsReducer.swift index 514f42cb..7b3b51dc 100644 --- a/EhPanda/View/Detail/Comments/CommentsReducer.swift +++ b/EhPanda/View/Detail/Comments/CommentsReducer.swift @@ -22,19 +22,20 @@ struct CommentsReducer { case postComment, voteComment, fetchGallery } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var commentContent = "" - @BindingState var postCommentFocused = false + var route: Route? + var commentContent = "" + var postCommentFocused = false var hudConfig: TTProgressHUDConfig = .loading var scrollCommentID: String? var scrollRowOpacity: Double = 1 - @Heap var detailState: DetailReducer.State! + var detailState: Heap init() { - _detailState = .init(.init()) + detailState = .init(.init()) } } @@ -74,12 +75,12 @@ struct CommentsReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - case .binding: return .none @@ -88,7 +89,7 @@ struct CommentsReducer { return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: - state.detailState = .init() + state.detailState.wrappedValue = .init() state.commentContent = .init() state.postCommentFocused = false return .send(.detail(.teardown)) @@ -153,7 +154,7 @@ struct CommentsReducer { } ) } else if let commentID = commentID { - state.detailState.commentsState?.scrollCommentID = commentID + state.detailState.wrappedValue?.commentsState.wrappedValue?.scrollCommentID = commentID effects.append( .run { send in try await Task.sleep(for: .milliseconds(750)) @@ -171,8 +172,8 @@ struct CommentsReducer { } case .onAppear: - if state.detailState == nil { - state.detailState = .init() + if state.detailState.wrappedValue == nil { + state.detailState.wrappedValue = .init() } return state.scrollCommentID != nil ? .send(.performScrollOpacityEffect) : .none diff --git a/EhPanda/View/Detail/Comments/CommentsView.swift b/EhPanda/View/Detail/Comments/CommentsView.swift index b13ea4d5..fde5b52b 100644 --- a/EhPanda/View/Detail/Comments/CommentsView.swift +++ b/EhPanda/View/Detail/Comments/CommentsView.swift @@ -10,8 +10,7 @@ import Kingfisher import ComposableArchitecture struct CommentsView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let gid: String private let token: String private let apiKey: String @@ -29,7 +28,6 @@ struct CommentsView: View { blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.gid = gid self.token = token self.apiKey = apiKey @@ -47,16 +45,16 @@ struct CommentsView: View { List(comments) { comment in CommentCell( gid: gid, comment: comment, - linkAction: { viewStore.send(.handleCommentLink($0)) } + linkAction: { store.send(.handleCommentLink($0)) } ) .opacity( - comment.commentID == viewStore.scrollCommentID - ? viewStore.scrollRowOpacity : 1 + comment.commentID == store.scrollCommentID + ? store.scrollRowOpacity : 1 ) .swipeActions(edge: .leading) { if comment.votable { Button { - viewStore.send(.voteComment(gid, token, apiKey, comment.commentID, -1)) + store.send(.voteComment(gid, token, apiKey, comment.commentID, -1)) } label: { Image(systemSymbol: .handThumbsdown) } @@ -66,7 +64,7 @@ struct CommentsView: View { .swipeActions(edge: .trailing) { if comment.votable { Button { - viewStore.send(.voteComment(gid, token, apiKey, comment.commentID, 1)) + store.send(.voteComment(gid, token, apiKey, comment.commentID, 1)) } label: { Image(systemSymbol: .handThumbsup) } @@ -74,8 +72,8 @@ struct CommentsView: View { } if comment.editable { Button { - viewStore.send(.setCommentContent(comment.plainTextContent)) - viewStore.send(.setNavigation(.postComment(comment.commentID))) + store.send(.setCommentContent(comment.plainTextContent)) + store.send(.setNavigation(.postComment(comment.commentID))) } label: { Image(systemSymbol: .squareAndPencil) } @@ -83,7 +81,7 @@ struct CommentsView: View { } } .onAppear { - if let scrollCommentID = viewStore.scrollCommentID { + if let scrollCommentID = store.scrollCommentID { DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { withAnimation { proxy.scrollTo(scrollCommentID, anchor: .top) @@ -92,36 +90,36 @@ struct CommentsView: View { } } } - .sheet(unwrapping: viewStore.$route, case: /CommentsReducer.Route.postComment) { route in + .sheet(unwrapping: $store.route, case: /CommentsReducer.Route.postComment) { route in let hasCommentID = !route.wrappedValue.isEmpty PostCommentView( title: hasCommentID ? L10n.Localizable.PostCommentView.Title.editComment : L10n.Localizable.PostCommentView.Title.postComment, - content: viewStore.$commentContent, - isFocused: viewStore.$postCommentFocused, + content: $store.commentContent, + isFocused: $store.postCommentFocused, postAction: { if hasCommentID { - viewStore.send(.postComment(galleryURL, route.wrappedValue)) + store.send(.postComment(galleryURL, route.wrappedValue)) } else { - viewStore.send(.postComment(galleryURL)) + store.send(.postComment(galleryURL)) } - viewStore.send(.setNavigation(nil)) + store.send(.setNavigation(nil)) }, - cancelAction: { viewStore.send(.setNavigation(nil)) }, - onAppearAction: { viewStore.send(.onPostCommentAppear) } + cancelAction: { store.send(.setNavigation(nil)) }, + onAppearAction: { store.send(.onPostCommentAppear) } ) .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } .progressHUD( - config: viewStore.hudConfig, - unwrapping: viewStore.$route, + config: store.hudConfig, + unwrapping: $store.route, case: /CommentsReducer.Route.hud ) - .animation(.default, value: viewStore.scrollRowOpacity) + .animation(.default, value: store.scrollRowOpacity) .onAppear { - viewStore.send(.onAppear) + store.send(.onAppear) } .background(navigationLink) .toolbar(content: toolbar) @@ -131,7 +129,7 @@ struct CommentsView: View { private func toolbar() -> some ToolbarContent { CustomToolbarItem { Button { - viewStore.send(.setNavigation(.postComment(""))) + store.send(.setNavigation(.postComment(""))) } label: { Image(systemSymbol: .squareAndPencil) } @@ -143,9 +141,9 @@ struct CommentsView: View { // MARK: NavigationLinks private extension CommentsView { @ViewBuilder var navigationLink: some View { - NavigationLink(unwrapping: viewStore.$route, case: /CommentsReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: /CommentsReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) diff --git a/EhPanda/View/Detail/DetailReducer.swift b/EhPanda/View/Detail/DetailReducer.swift index a6661577..edac3a5e 100644 --- a/EhPanda/View/Detail/DetailReducer.swift +++ b/EhPanda/View/Detail/DetailReducer.swift @@ -30,10 +30,11 @@ struct DetailReducer { case fetchDatabaseInfos, fetchGalleryDetail, rateGallery, favorGallery, unfavorGallery, postComment, voteTag } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var commentContent = "" - @BindingState var postCommentFocused = false + var route: Route? + var commentContent = "" + var postCommentFocused = false var showsNewDawnGreeting = false var showsUserRating = false @@ -52,13 +53,13 @@ struct DetailReducer { var archivesState = ArchivesReducer.State() var torrentsState = TorrentsReducer.State() var previewsState = PreviewsReducer.State() - @Heap var commentsState: CommentsReducer.State? + var commentsState: Heap var galleryInfosState = GalleryInfosReducer.State() - @Heap var detailSearchState: DetailSearchReducer.State? + var detailSearchState: Heap init() { - _commentsState = .init(nil) - _detailSearchState = .init(nil) + commentsState = .init(nil) + detailSearchState = .init(nil) } mutating func updateRating(value: DragGesture.Value) { @@ -120,12 +121,12 @@ struct DetailReducer { var body: some Reducer { RecurseReducer { (self) in BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - case .binding: return .none @@ -138,11 +139,11 @@ struct DetailReducer { state.archivesState = .init() state.torrentsState = .init() state.previewsState = .init() - state.commentsState = .init() + state.commentsState.wrappedValue = .init() state.commentContent = .init() state.postCommentFocused = false state.galleryInfosState = .init() - state.detailSearchState = .init() + state.detailSearchState.wrappedValue = .init() return .merge( .send(.reading(.teardown)), .send(.archives(.teardown)), @@ -160,11 +161,11 @@ struct DetailReducer { case .onAppear(let gid, let showsNewDawnGreeting): state.showsNewDawnGreeting = showsNewDawnGreeting - if state.detailSearchState == nil { - state.detailSearchState = .init() + if state.detailSearchState.wrappedValue == nil { + state.detailSearchState.wrappedValue = .init() } - if state.commentsState == nil { - state.commentsState = .init() + if state.commentsState.wrappedValue == nil { + state.commentsState.wrappedValue = .init() } return .send(.fetchDatabaseInfos(gid)) @@ -397,9 +398,11 @@ struct DetailReducer { return .send(.anyGalleryOpsDone(result)) case .comments(.detail(let recursiveAction)): - guard state.commentsState != nil else { return .none } - return self.reduce(into: &state.commentsState!.detailState, action: recursiveAction) - .map({ Action.comments(.detail($0)) }) + guard state.commentsState.wrappedValue != nil else { return .none } + return self.reduce( + into: &state.commentsState.wrappedValue!.detailState.wrappedValue!, action: recursiveAction + ) + .map({ Action.comments(.detail($0)) }) case .comments: return .none @@ -408,21 +411,23 @@ struct DetailReducer { return .none case .detailSearch(.detail(let recursiveAction)): - guard state.detailSearchState != nil else { return .none } - return self.reduce(into: &state.detailSearchState!.detailState, action: recursiveAction) - .map({ Action.detailSearch(.detail($0)) }) + guard state.detailSearchState.wrappedValue != nil else { return .none } + return self.reduce( + into: &state.detailSearchState.wrappedValue!.detailState.wrappedValue!, action: recursiveAction + ) + .map({ Action.detailSearch(.detail($0)) }) case .detailSearch: return .none } } .ifLet( - \.commentsState, + \.commentsState.wrappedValue, action: /Action.comments, then: CommentsReducer.init ) .ifLet( - \.detailSearchState, + \.detailSearchState.wrappedValue, action: /Action.detailSearch, then: DetailSearchReducer.init ) diff --git a/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift b/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift index 516a63d7..3730e13a 100644 --- a/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift +++ b/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift @@ -20,9 +20,10 @@ struct DetailSearchReducer { case fetchGalleries, fetchMoreGalleries } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var keyword = "" + var route: Route? + var keyword = "" var lastKeyword = "" var galleries = [Gallery]() @@ -30,12 +31,12 @@ struct DetailSearchReducer { var loadingState: LoadingState = .idle var footerLoadingState: LoadingState = .idle - @Heap var detailState: DetailReducer.State! + var detailState: Heap var filtersState = FiltersReducer.State() var quickDetailSearchState = QuickSearchReducer.State() init() { - _detailState = .init(.init()) + detailState = .init(.init()) } mutating func insertGalleries(_ galleries: [Gallery]) { @@ -68,18 +69,20 @@ struct DetailSearchReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } + .onChange(of: \.keyword) { _, newValue in + Reduce { state, _ in + if !newValue.isEmpty { + state.lastKeyword = newValue + } + return .none + } + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - - case .binding(\.$keyword): - if !state.keyword.isEmpty { - state.lastKeyword = state.keyword - } - return .none - case .binding: return .none @@ -88,7 +91,7 @@ struct DetailSearchReducer { return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: - state.detailState = .init() + state.detailState.wrappedValue = .init() state.filtersState = .init() state.quickDetailSearchState = .init() return .merge( diff --git a/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift b/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift index 3850c967..8354ac07 100644 --- a/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift +++ b/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift @@ -9,8 +9,7 @@ import SwiftUI import ComposableArchitecture struct DetailSearchView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let keyword: String private let user: User @Binding private var setting: Setting @@ -22,7 +21,6 @@ struct DetailSearchView: View { keyword: String, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.keyword = keyword self.user = user _setting = setting @@ -33,59 +31,59 @@ struct DetailSearchView: View { var body: some View { let content = GenericList( - galleries: viewStore.galleries, + galleries: store.galleries, setting: setting, - pageNumber: viewStore.pageNumber, - loadingState: viewStore.loadingState, - footerLoadingState: viewStore.footerLoadingState, - fetchAction: { viewStore.send(.fetchGalleries()) }, - fetchMoreAction: { viewStore.send(.fetchMoreGalleries) }, - navigateAction: { viewStore.send(.setNavigation(.detail($0))) }, + pageNumber: store.pageNumber, + loadingState: store.loadingState, + footerLoadingState: store.footerLoadingState, + fetchAction: { store.send(.fetchGalleries()) }, + fetchMoreAction: { store.send(.fetchMoreGalleries) }, + navigateAction: { store.send(.setNavigation(.detail($0))) }, translateAction: { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) - .sheet(unwrapping: viewStore.$route, case: /DetailSearchReducer.Route.quickSearch) { _ in + .sheet(unwrapping: $store.route, case: /DetailSearchReducer.Route.quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickDetailSearchState, action: \.quickSearch) ) { keyword in - viewStore.send(.setNavigation(nil)) - viewStore.send(.fetchGalleries(keyword)) + store.send(.setNavigation(nil)) + store.send(.fetchGalleries(keyword)) } .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.$route, case: /DetailSearchReducer.Route.filters) { _ in + .sheet(unwrapping: $store.route, case: /DetailSearchReducer.Route.filters) { _ in FiltersView(store: store.scope(state: \.filtersState, action: \.filters)) .accentColor(setting.accentColor).autoBlur(radius: blurRadius) } - .searchable(text: viewStore.$keyword) + .searchable(text: $store.keyword) .searchSuggestions { TagSuggestionView( - keyword: viewStore.$keyword, translations: tagTranslator.translations, + keyword: $store.keyword, translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion ) } .onSubmit(of: .search) { - viewStore.send(.fetchGalleries()) + store.send(.fetchGalleries()) } .onAppear { - if viewStore.galleries.isEmpty { + if store.galleries.isEmpty { DispatchQueue.main.async { - viewStore.send(.fetchGalleries(keyword)) + store.send(.fetchGalleries(keyword)) } } } .background(navigationLink) .toolbar(content: toolbar) - .navigationTitle(viewStore.lastKeyword) + .navigationTitle(store.lastKeyword) if DeviceUtil.isPad { content - .sheet(unwrapping: viewStore.$route, case: /DetailSearchReducer.Route.detail) { route in + .sheet(unwrapping: $store.route, case: /DetailSearchReducer.Route.detail) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -99,9 +97,9 @@ struct DetailSearchView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.$route, case: /DetailSearchReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: /DetailSearchReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -112,10 +110,10 @@ struct DetailSearchView: View { CustomToolbarItem { ToolbarFeaturesMenu { FiltersButton { - viewStore.send(.setNavigation(.filters)) + store.send(.setNavigation(.filters)) } QuickSearchButton { - viewStore.send(.setNavigation(.quickSearch)) + store.send(.setNavigation(.quickSearch)) } } } diff --git a/EhPanda/View/Detail/DetailView.swift b/EhPanda/View/Detail/DetailView.swift index 0a0c75e2..e5ec7aa1 100644 --- a/EhPanda/View/Detail/DetailView.swift +++ b/EhPanda/View/Detail/DetailView.swift @@ -11,8 +11,7 @@ import ComposableArchitecture import CommonMark struct DetailView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let gid: String private let user: User @Binding private var setting: Setting @@ -24,7 +23,6 @@ struct DetailView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.gid = gid self.user = user _setting = setting @@ -37,91 +35,91 @@ struct DetailView: View { ScrollView(showsIndicators: false) { VStack(spacing: 30) { HeaderSection( - gallery: viewStore.gallery, - galleryDetail: viewStore.galleryDetail ?? .empty, + gallery: store.gallery, + galleryDetail: store.galleryDetail ?? .empty, user: user, displaysJapaneseTitle: setting.displaysJapaneseTitle, - showFullTitle: viewStore.showsFullTitle, - showFullTitleAction: { viewStore.send(.toggleShowFullTitle) }, - favorAction: { viewStore.send(.favorGallery($0)) }, - unfavorAction: { viewStore.send(.unfavorGallery) }, - navigateReadingAction: { viewStore.send(.setNavigation(.reading)) }, + showFullTitle: store.showsFullTitle, + showFullTitleAction: { store.send(.toggleShowFullTitle) }, + favorAction: { store.send(.favorGallery($0)) }, + unfavorAction: { store.send(.unfavorGallery) }, + navigateReadingAction: { store.send(.setNavigation(.reading)) }, navigateUploaderAction: { - if let uploader = viewStore.galleryDetail?.uploader { + if let uploader = store.galleryDetail?.uploader { let keyword = "uploader:" + "\"\(uploader)\"" - viewStore.send(.setNavigation(.detailSearch(keyword))) + store.send(.setNavigation(.detailSearch(keyword))) } } ) .padding(.horizontal) DescriptionSection( - gallery: viewStore.gallery, - galleryDetail: viewStore.galleryDetail ?? .empty, + gallery: store.gallery, + galleryDetail: store.galleryDetail ?? .empty, navigateGalleryInfosAction: { - if let galleryDetail = viewStore.galleryDetail { - viewStore.send(.setNavigation(.galleryInfos(viewStore.gallery, galleryDetail))) + if let galleryDetail = store.galleryDetail { + store.send(.setNavigation(.galleryInfos(store.gallery, galleryDetail))) } } ) ActionSection( - galleryDetail: viewStore.galleryDetail ?? .empty, - userRating: viewStore.userRating, - showUserRating: viewStore.showsUserRating, - showUserRatingAction: { viewStore.send(.toggleShowUserRating) }, - updateRatingAction: { viewStore.send(.updateRating($0)) }, - confirmRatingAction: { viewStore.send(.confirmRating($0)) }, + galleryDetail: store.galleryDetail ?? .empty, + userRating: store.userRating, + showUserRating: store.showsUserRating, + showUserRatingAction: { store.send(.toggleShowUserRating) }, + updateRatingAction: { store.send(.updateRating($0)) }, + confirmRatingAction: { store.send(.confirmRating($0)) }, navigateSimilarGalleryAction: { - if let trimmedTitle = viewStore.galleryDetail?.trimmedTitle { - viewStore.send(.setNavigation(.detailSearch(trimmedTitle))) + if let trimmedTitle = store.galleryDetail?.trimmedTitle { + store.send(.setNavigation(.detailSearch(trimmedTitle))) } } ) - if !viewStore.galleryTags.isEmpty { + if !store.galleryTags.isEmpty { TagsSection( - tags: viewStore.galleryTags, showsImages: setting.showsImagesInTags, - voteTagAction: { viewStore.send(.voteTag($0, $1)) }, - navigateSearchAction: { viewStore.send(.setNavigation(.detailSearch($0))) }, - navigateTagDetailAction: { viewStore.send(.setNavigation(.tagDetail($0))) }, + tags: store.galleryTags, showsImages: setting.showsImagesInTags, + voteTagAction: { store.send(.voteTag($0, $1)) }, + navigateSearchAction: { store.send(.setNavigation(.detailSearch($0))) }, + navigateTagDetailAction: { store.send(.setNavigation(.tagDetail($0))) }, translateAction: { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) .padding(.horizontal) } - if !viewStore.galleryPreviewURLs.isEmpty { + if !store.galleryPreviewURLs.isEmpty { PreviewsSection( - pageCount: viewStore.galleryDetail?.pageCount ?? 0, - previewURLs: viewStore.galleryPreviewURLs, - navigatePreviewsAction: { viewStore.send(.setNavigation(.previews)) }, + pageCount: store.galleryDetail?.pageCount ?? 0, + previewURLs: store.galleryPreviewURLs, + navigatePreviewsAction: { store.send(.setNavigation(.previews)) }, navigateReadingAction: { - viewStore.send(.updateReadingProgress($0)) - viewStore.send(.setNavigation(.reading)) + store.send(.updateReadingProgress($0)) + store.send(.setNavigation(.reading)) } ) } CommentsSection( - comments: viewStore.galleryComments, + comments: store.galleryComments, navigateCommentAction: { - if let galleryURL = viewStore.gallery.galleryURL { - viewStore.send(.setNavigation(.comments(galleryURL))) + if let galleryURL = store.gallery.galleryURL { + store.send(.setNavigation(.comments(galleryURL))) } }, - navigatePostCommentAction: { viewStore.send(.setNavigation(.postComment)) } + navigatePostCommentAction: { store.send(.setNavigation(.postComment)) } ) } .padding(.bottom, 20) .padding(.top, -25) } - .opacity(viewStore.galleryDetail == nil ? 0 : 1) + .opacity(store.galleryDetail == nil ? 0 : 1) LoadingView() .opacity( - viewStore.galleryDetail == nil - && viewStore.loadingState == .loading ? 1 : 0 + store.galleryDetail == nil + && store.loadingState == .loading ? 1 : 0 ) - let error = (/LoadingState.failed).extract(from: viewStore.loadingState) - let retryAction: () -> Void = { viewStore.send(.fetchGalleryDetail) } + let error = (/LoadingState.failed).extract(from: store.loadingState) + let retryAction: () -> Void = { store.send(.fetchGalleryDetail) } ErrorView(error: error ?? .unknown, action: error?.isRetryable != false ? retryAction : nil) - .opacity(viewStore.galleryDetail == nil && error != nil ? 1 : 0) + .opacity(store.galleryDetail == nil && error != nil ? 1 : 0) } - .fullScreenCover(unwrapping: viewStore.$route, case: /DetailReducer.Route.reading) { _ in + .fullScreenCover(unwrapping: $store.route, case: /DetailReducer.Route.reading) { _ in ReadingView( store: store.scope(state: \.readingState, action: \.reading), gid: gid, setting: $setting, blurRadius: blurRadius @@ -129,7 +127,7 @@ struct DetailView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.$route, case: /DetailReducer.Route.archives) { route in + .sheet(unwrapping: $store.route, case: /DetailReducer.Route.archives) { route in let (galleryURL, archiveURL) = route.wrappedValue ArchivesView( store: store.scope(state: \.archivesState, action: \.archives), @@ -138,47 +136,47 @@ struct DetailView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.$route, case: /DetailReducer.Route.torrents) { _ in + .sheet(unwrapping: $store.route, case: /DetailReducer.Route.torrents) { _ in TorrentsView( store: store.scope(state: \.torrentsState, action: \.torrents), - gid: gid, token: viewStore.gallery.token, blurRadius: blurRadius + gid: gid, token: store.gallery.token, blurRadius: blurRadius ) .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.$route, case: /DetailReducer.Route.share) { route in + .sheet(unwrapping: $store.route, case: /DetailReducer.Route.share) { route in ActivityView(activityItems: [route.wrappedValue]) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.$route, case: /DetailReducer.Route.postComment) { _ in + .sheet(unwrapping: $store.route, case: /DetailReducer.Route.postComment) { _ in PostCommentView( title: L10n.Localizable.PostCommentView.Title.postComment, - content: viewStore.$commentContent, - isFocused: viewStore.$postCommentFocused, + content: $store.commentContent, + isFocused: $store.postCommentFocused, postAction: { - if let galleryURL = viewStore.gallery.galleryURL { - viewStore.send(.postComment(galleryURL)) + if let galleryURL = store.gallery.galleryURL { + store.send(.postComment(galleryURL)) } - viewStore.send(.setNavigation(nil)) + store.send(.setNavigation(nil)) }, - cancelAction: { viewStore.send(.setNavigation(nil)) }, - onAppearAction: { viewStore.send(.onPostCommentAppear) } + cancelAction: { store.send(.setNavigation(nil)) }, + onAppearAction: { store.send(.onPostCommentAppear) } ) .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.$route, case: /DetailReducer.Route.newDawn) { route in + .sheet(unwrapping: $store.route, case: /DetailReducer.Route.newDawn) { route in NewDawnView(greeting: route.wrappedValue).autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.$route, case: /DetailReducer.Route.tagDetail) { route in + .sheet(unwrapping: $store.route, case: /DetailReducer.Route.tagDetail) { route in TagDetailView(detail: route.wrappedValue).autoBlur(radius: blurRadius) } - .animation(.default, value: viewStore.showsUserRating) - .animation(.default, value: viewStore.showsFullTitle) - .animation(.default, value: viewStore.galleryDetail) + .animation(.default, value: store.showsUserRating) + .animation(.default, value: store.showsFullTitle) + .animation(.default, value: store.galleryDetail) .onAppear { DispatchQueue.main.async { - viewStore.send(.onAppear(gid, setting.showsNewDawnGreeting)) + store.send(.onAppear(gid, setting.showsNewDawnGreeting)) } } .background(navigationLinks) @@ -189,31 +187,31 @@ struct DetailView: View { // MARK: NavigationLinks private extension DetailView { @ViewBuilder var navigationLinks: some View { - NavigationLink(unwrapping: viewStore.$route, case: /DetailReducer.Route.previews) { _ in + NavigationLink(unwrapping: $store.route, case: /DetailReducer.Route.previews) { _ in PreviewsView( store: store.scope(state: \.previewsState, action: \.previews), gid: gid, setting: $setting, blurRadius: blurRadius ) } - NavigationLink(unwrapping: viewStore.$route, case: /DetailReducer.Route.comments) { route in - IfLetStore(store.scope(state: \.commentsState, action: \.comments)) { store in + NavigationLink(unwrapping: $store.route, case: /DetailReducer.Route.comments) { route in + if let commentStore = store.scope(state: \.commentsState.wrappedValue, action: \.comments) { CommentsView( - store: store, gid: gid, token: viewStore.gallery.token, apiKey: viewStore.apiKey, - galleryURL: route.wrappedValue, comments: viewStore.galleryComments, user: user, + store: commentStore, gid: gid, token: store.gallery.token, apiKey: store.apiKey, + galleryURL: route.wrappedValue, comments: store.galleryComments, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } } - NavigationLink(unwrapping: viewStore.$route, case: /DetailReducer.Route.detailSearch) { route in - IfLetStore(store.scope(state: \.detailSearchState, action: \.detailSearch)) { store in + NavigationLink(unwrapping: $store.route, case: /DetailReducer.Route.detailSearch) { route in + if let detailSearchStore = store.scope(state: \.detailSearchState.wrappedValue, action: \.detailSearch) { DetailSearchView( - store: store, keyword: route.wrappedValue, user: user, setting: $setting, + store: detailSearchStore, keyword: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } } - NavigationLink(unwrapping: viewStore.$route, case: /DetailReducer.Route.galleryInfos) { route in + NavigationLink(unwrapping: $store.route, case: /DetailReducer.Route.galleryInfos) { route in let (gallery, galleryDetail) = route.wrappedValue GalleryInfosView( store: store.scope(state: \.galleryInfosState, action: \.galleryInfos), @@ -229,33 +227,33 @@ private extension DetailView { CustomToolbarItem { ToolbarFeaturesMenu { Button { - if let galleryURL = viewStore.gallery.galleryURL, - let archiveURL = viewStore.galleryDetail?.archiveURL + if let galleryURL = store.gallery.galleryURL, + let archiveURL = store.galleryDetail?.archiveURL { - viewStore.send(.setNavigation(.archives(galleryURL, archiveURL))) + store.send(.setNavigation(.archives(galleryURL, archiveURL))) } } label: { Label(L10n.Localizable.DetailView.ToolbarItem.Button.archives, systemSymbol: .docZipper) } - .disabled(viewStore.galleryDetail?.archiveURL == nil || !CookieUtil.didLogin) + .disabled(store.galleryDetail?.archiveURL == nil || !CookieUtil.didLogin) Button { - viewStore.send(.setNavigation(.torrents)) + store.send(.setNavigation(.torrents)) } label: { let base = L10n.Localizable.DetailView.ToolbarItem.Button.torrents - let torrentCount = viewStore.galleryDetail?.torrentCount ?? 0 + let torrentCount = store.galleryDetail?.torrentCount ?? 0 let baseWithCount = [base, "(\(torrentCount))"].joined(separator: " ") Label(torrentCount > 0 ? baseWithCount : base, systemSymbol: .leaf) } - .disabled((viewStore.galleryDetail?.torrentCount ?? 0 > 0) != true) + .disabled((store.galleryDetail?.torrentCount ?? 0 > 0) != true) Button { - if let galleryURL = viewStore.gallery.galleryURL { - viewStore.send(.setNavigation(.share(galleryURL))) + if let galleryURL = store.gallery.galleryURL { + store.send(.setNavigation(.share(galleryURL))) } } label: { Label(L10n.Localizable.DetailView.ToolbarItem.Button.share, systemSymbol: .squareAndArrowUp) } } - .disabled(viewStore.galleryDetail == nil || viewStore.loadingState == .loading) + .disabled(store.galleryDetail == nil || store.loadingState == .loading) } } } diff --git a/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift b/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift index 67ba7331..88f65386 100644 --- a/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift +++ b/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift @@ -14,8 +14,9 @@ struct GalleryInfosReducer { case hud } + @ObservableState struct State: Equatable { - @BindingState var route: Route? + var route: Route? var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded } diff --git a/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift b/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift index 278dbae8..fa017d10 100644 --- a/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift +++ b/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift @@ -9,14 +9,12 @@ import SwiftUI import ComposableArchitecture struct GalleryInfosView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let gallery: Gallery private let galleryDetail: GalleryDetail init(store: StoreOf, gallery: Gallery, galleryDetail: GalleryDetail) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.gallery = gallery self.galleryDetail = galleryDetail } @@ -106,7 +104,7 @@ struct GalleryInfosView: View { Spacer() Button { if let text = info.value { - viewStore.send(.copyText(text)) + store.send(.copyText(text)) } } label: { Text(info.value ?? L10n.Localizable.GalleryInfosView.Value.none) @@ -117,8 +115,8 @@ struct GalleryInfosView: View { } } .progressHUD( - config: viewStore.hudConfig, - unwrapping: viewStore.$route, + config: store.hudConfig, + unwrapping: $store.route, case: /GalleryInfosReducer.Route.hud ) .navigationTitle(L10n.Localizable.GalleryInfosView.Title.galleryInfos) diff --git a/EhPanda/View/Detail/Previews/PreviewsReducer.swift b/EhPanda/View/Detail/Previews/PreviewsReducer.swift index 77b718df..c943ed3d 100644 --- a/EhPanda/View/Detail/Previews/PreviewsReducer.swift +++ b/EhPanda/View/Detail/Previews/PreviewsReducer.swift @@ -18,8 +18,9 @@ struct PreviewsReducer { case fetchDatabaseInfos, fetchPreviewURLs } + @ObservableState struct State: Equatable { - @BindingState var route: Route? + var route: Route? var gallery: Gallery = .empty var loadingState: LoadingState = .idle @@ -59,12 +60,12 @@ struct PreviewsReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - case .binding: return .none diff --git a/EhPanda/View/Detail/Previews/PreviewsView.swift b/EhPanda/View/Detail/Previews/PreviewsView.swift index b44cbfe6..a2221e43 100644 --- a/EhPanda/View/Detail/Previews/PreviewsView.swift +++ b/EhPanda/View/Detail/Previews/PreviewsView.swift @@ -10,8 +10,7 @@ import Kingfisher import ComposableArchitecture struct PreviewsView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let gid: String @Binding private var setting: Setting private let blurRadius: Double @@ -21,7 +20,6 @@ struct PreviewsView: View { gid: String, setting: Binding, blurRadius: Double ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.gid = gid _setting = setting self.blurRadius = blurRadius @@ -40,35 +38,35 @@ struct PreviewsView: View { var body: some View { ScrollView { LazyVGrid(columns: gridItems) { - ForEach(1.. - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let gid: String private let token: String private let blurRadius: Double init(store: StoreOf, gid: String, token: String, blurRadius: Double) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.gid = gid self.token = token self.blurRadius = blurRadius @@ -26,37 +24,37 @@ struct TorrentsView: View { var body: some View { NavigationView { ZStack { - List(viewStore.torrents) { torrent in + List(store.torrents) { torrent in TorrentRow(torrent: torrent) { magnetURL in - viewStore.send(.copyText(magnetURL)) + store.send(.copyText(magnetURL)) } .swipeActions { Button { - viewStore.send(.fetchTorrent(torrent.hash, torrent.torrentURL)) + store.send(.fetchTorrent(torrent.hash, torrent.torrentURL)) } label: { Image(systemSymbol: .arrowDownDocFill) } } } - LoadingView().opacity(viewStore.loadingState == .loading && viewStore.torrents.isEmpty ? 1 : 0) - let error = (/LoadingState.failed).extract(from: viewStore.loadingState) + LoadingView().opacity(store.loadingState == .loading && store.torrents.isEmpty ? 1 : 0) + let error = (/LoadingState.failed).extract(from: store.loadingState) ErrorView(error: error ?? .unknown) { - viewStore.send(.fetchGalleryTorrents(gid, token)) + store.send(.fetchGalleryTorrents(gid, token)) } - .opacity(error != nil && viewStore.torrents.isEmpty ? 1 : 0) + .opacity(error != nil && store.torrents.isEmpty ? 1 : 0) } - .sheet(unwrapping: viewStore.$route, case: /TorrentsReducer.Route.share) { route in + .sheet(unwrapping: $store.route, case: /TorrentsReducer.Route.share) { route in ActivityView(activityItems: [route.wrappedValue]) .autoBlur(radius: blurRadius) } .progressHUD( - config: viewStore.hudConfig, - unwrapping: viewStore.$route, + config: store.hudConfig, + unwrapping: $store.route, case: /TorrentsReducer.Route.hud ) - .animation(.default, value: viewStore.torrents) + .animation(.default, value: store.torrents) .onAppear { - viewStore.send(.fetchGalleryTorrents(gid, token)) + store.send(.fetchGalleryTorrents(gid, token)) } .navigationTitle(L10n.Localizable.TorrentsView.Title.torrents) } diff --git a/EhPanda/View/Favorites/FavoritesReducer.swift b/EhPanda/View/Favorites/FavoritesReducer.swift index 2f1a054d..e140de18 100644 --- a/EhPanda/View/Favorites/FavoritesReducer.swift +++ b/EhPanda/View/Favorites/FavoritesReducer.swift @@ -17,9 +17,10 @@ struct FavoritesReducer { case detail(String) } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var keyword = "" + var route: Route? + var keyword = "" var index = -1 var sortOrder: FavoritesSortOrder? @@ -42,11 +43,11 @@ struct FavoritesReducer { rawFooterLoadingState[index] } - @Heap var detailState: DetailReducer.State! + var detailState: Heap var quickSearchState = QuickSearchReducer.State() init() { - _detailState = .init(.init()) + detailState = .init(.init()) } mutating func insertGalleries(index: Int, galleries: [Gallery]) { @@ -79,12 +80,12 @@ struct FavoritesReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - case .binding: return .none @@ -98,7 +99,7 @@ struct FavoritesReducer { return .send(.fetchGalleries()) case .clearSubStates: - state.detailState = .init() + state.detailState.wrappedValue = .init() return .send(.detail(.teardown)) case .onNotLoginViewButtonTapped: @@ -196,7 +197,7 @@ struct FavoritesReducer { hapticsClient: hapticsClient ) - Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) } } diff --git a/EhPanda/View/Favorites/FavoritesView.swift b/EhPanda/View/Favorites/FavoritesView.swift index 0227a92a..712f2239 100644 --- a/EhPanda/View/Favorites/FavoritesView.swift +++ b/EhPanda/View/Favorites/FavoritesView.swift @@ -10,8 +10,7 @@ import AlertKit import ComposableArchitecture struct FavoritesView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double @@ -22,7 +21,6 @@ struct FavoritesView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -30,8 +28,8 @@ struct FavoritesView: View { } private var navigationTitle: String { - let favoriteCategory = user.getFavoriteCategory(index: viewStore.index) - return (viewStore.index == -1 ? L10n.Localizable.FavoritesView.Title.favorites : favoriteCategory) + let favoriteCategory = user.getFavoriteCategory(index: store.index) + return (store.index == -1 ? L10n.Localizable.FavoritesView.Title.favorites : favoriteCategory) } var body: some View { @@ -40,46 +38,46 @@ struct FavoritesView: View { ZStack { if CookieUtil.didLogin { GenericList( - galleries: viewStore.galleries ?? [], + galleries: store.galleries ?? [], setting: setting, - pageNumber: viewStore.pageNumber, - loadingState: viewStore.loadingState ?? .idle, - footerLoadingState: viewStore.footerLoadingState ?? .idle, - fetchAction: { viewStore.send(.fetchGalleries()) }, - fetchMoreAction: { viewStore.send(.fetchMoreGalleries) }, - navigateAction: { viewStore.send(.setNavigation(.detail($0))) }, + pageNumber: store.pageNumber, + loadingState: store.loadingState ?? .idle, + footerLoadingState: store.footerLoadingState ?? .idle, + fetchAction: { store.send(.fetchGalleries()) }, + fetchMoreAction: { store.send(.fetchMoreGalleries) }, + navigateAction: { store.send(.setNavigation(.detail($0))) }, translateAction: { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) } else { - NotLoginView(action: { viewStore.send(.onNotLoginViewButtonTapped) }) + NotLoginView(action: { store.send(.onNotLoginViewButtonTapped) }) } } - .sheet(unwrapping: viewStore.$route, case: /FavoritesReducer.Route.quickSearch) { _ in + .sheet(unwrapping: $store.route, case: /FavoritesReducer.Route.quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickSearchState, action: \.quickSearch) ) { keyword in - viewStore.send(.setNavigation(nil)) - viewStore.send(.fetchGalleries(keyword)) + store.send(.setNavigation(nil)) + store.send(.fetchGalleries(keyword)) } .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .searchable(text: viewStore.$keyword) + .searchable(text: $store.keyword) .searchSuggestions { TagSuggestionView( - keyword: viewStore.$keyword, translations: tagTranslator.translations, + keyword: $store.keyword, translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion ) } .onSubmit(of: .search) { - viewStore.send(.fetchGalleries()) + store.send(.fetchGalleries()) } .onAppear { - if viewStore.galleries?.isEmpty != false && CookieUtil.didLogin { + if store.galleries?.isEmpty != false && CookieUtil.didLogin { DispatchQueue.main.async { - viewStore.send(.fetchGalleries()) + store.send(.fetchGalleries()) } } } @@ -89,10 +87,10 @@ struct FavoritesView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: viewStore.$route, case: /FavoritesReducer.Route.detail) { route in + .sheet(unwrapping: $store.route, case: /FavoritesReducer.Route.detail) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -107,9 +105,9 @@ struct FavoritesView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.$route, case: /FavoritesReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: /FavoritesReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -118,18 +116,18 @@ struct FavoritesView: View { } private func toolbar() -> some ToolbarContent { CustomToolbarItem(tint: .primary) { - FavoritesIndexMenu(user: user, index: viewStore.index) { index in - if index != viewStore.index { - viewStore.send(.setFavoritesIndex(index)) + FavoritesIndexMenu(user: user, index: store.index) { index in + if index != store.index { + store.send(.setFavoritesIndex(index)) } } - SortOrderMenu(sortOrder: viewStore.sortOrder) { order in - if viewStore.sortOrder != order { - viewStore.send(.fetchGalleries(nil, order)) + SortOrderMenu(sortOrder: store.sortOrder) { order in + if store.sortOrder != order { + store.send(.fetchGalleries(nil, order)) } } QuickSearchButton(hideText: true) { - viewStore.send(.setNavigation(.quickSearch)) + store.send(.setNavigation(.quickSearch)) } } } diff --git a/EhPanda/View/Home/Frontpage/FrontpageReducer.swift b/EhPanda/View/Home/Frontpage/FrontpageReducer.swift index 051234a9..c5f29449 100644 --- a/EhPanda/View/Home/Frontpage/FrontpageReducer.swift +++ b/EhPanda/View/Home/Frontpage/FrontpageReducer.swift @@ -19,9 +19,10 @@ struct FrontpageReducer { case fetchGalleries, fetchMoreGalleries } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var keyword = "" + var route: Route? + var keyword = "" var filteredGalleries: [Gallery] { guard !keyword.isEmpty else { return galleries } @@ -33,10 +34,10 @@ struct FrontpageReducer { var footerLoadingState: LoadingState = .idle var filtersState = FiltersReducer.State() - @Heap var detailState: DetailReducer.State! + var detailState: Heap init() { - _detailState = .init(.init()) + detailState = .init(.init()) } mutating func insertGalleries(_ galleries: [Gallery]) { @@ -68,12 +69,12 @@ struct FrontpageReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - case .binding: return .none @@ -82,7 +83,7 @@ struct FrontpageReducer { return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: - state.detailState = .init() + state.detailState.wrappedValue = .init() state.filtersState = .init() return .send(.detail(.teardown)) @@ -167,6 +168,6 @@ struct FrontpageReducer { ) Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) - Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Home/Frontpage/FrontpageView.swift b/EhPanda/View/Home/Frontpage/FrontpageView.swift index 4f147025..cfccd45f 100644 --- a/EhPanda/View/Home/Frontpage/FrontpageView.swift +++ b/EhPanda/View/Home/Frontpage/FrontpageView.swift @@ -10,8 +10,7 @@ import AlertKit import ComposableArchitecture struct FrontpageView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double @@ -22,7 +21,6 @@ struct FrontpageView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -32,27 +30,27 @@ struct FrontpageView: View { var body: some View { let content = GenericList( - galleries: viewStore.filteredGalleries, + galleries: store.filteredGalleries, setting: setting, - pageNumber: viewStore.pageNumber, - loadingState: viewStore.loadingState, - footerLoadingState: viewStore.footerLoadingState, - fetchAction: { viewStore.send(.fetchGalleries) }, - fetchMoreAction: { viewStore.send(.fetchMoreGalleries) }, - navigateAction: { viewStore.send(.setNavigation(.detail($0))) }, + pageNumber: store.pageNumber, + loadingState: store.loadingState, + footerLoadingState: store.footerLoadingState, + fetchAction: { store.send(.fetchGalleries) }, + fetchMoreAction: { store.send(.fetchMoreGalleries) }, + navigateAction: { store.send(.setNavigation(.detail($0))) }, translateAction: { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) - .sheet(unwrapping: viewStore.$route, case: /FrontpageReducer.Route.filters) { _ in + .sheet(unwrapping: $store.route, case: /FrontpageReducer.Route.filters) { _ in FiltersView(store: store.scope(state: \.filtersState, action: \.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } - .searchable(text: viewStore.$keyword, prompt: L10n.Localizable.Searchable.Prompt.filter) + .searchable(text: $store.keyword, prompt: L10n.Localizable.Searchable.Prompt.filter) .onAppear { - if viewStore.galleries.isEmpty { + if store.galleries.isEmpty { DispatchQueue.main.async { - viewStore.send(.fetchGalleries) + store.send(.fetchGalleries) } } } @@ -62,10 +60,10 @@ struct FrontpageView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: viewStore.$route, case: /FrontpageReducer.Route.detail) { route in + .sheet(unwrapping: $store.route, case: /FrontpageReducer.Route.detail) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -79,9 +77,9 @@ struct FrontpageView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.$route, case: /FrontpageReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: /FrontpageReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -91,7 +89,7 @@ struct FrontpageView: View { private func toolbar() -> some ToolbarContent { CustomToolbarItem { FiltersButton(hideText: true) { - viewStore.send(.setNavigation(.filters)) + store.send(.setNavigation(.filters)) } } } diff --git a/EhPanda/View/Home/History/HistoryReducer.swift b/EhPanda/View/Home/History/HistoryReducer.swift index d49ced81..97a632c2 100644 --- a/EhPanda/View/Home/History/HistoryReducer.swift +++ b/EhPanda/View/Home/History/HistoryReducer.swift @@ -16,10 +16,11 @@ struct HistoryReducer { case clearHistory } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var keyword = "" - @BindingState var clearDialogPresented = false + var route: Route? + var keyword = "" + var clearDialogPresented = false var filteredGalleries: [Gallery] { guard !keyword.isEmpty else { return galleries } @@ -28,10 +29,10 @@ struct HistoryReducer { var galleries = [Gallery]() var loadingState: LoadingState = .idle - @Heap var detailState: DetailReducer.State! + var detailState: Heap init() { - _detailState = .init(.init()) + detailState = .init(.init()) } } @@ -52,12 +53,12 @@ struct HistoryReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - case .binding: return .none @@ -66,7 +67,7 @@ struct HistoryReducer { return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: - state.detailState = .init() + state.detailState.wrappedValue = .init() return .send(.detail(.teardown)) case .clearHistoryGalleries: @@ -100,6 +101,6 @@ struct HistoryReducer { } } - Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Home/History/HistoryView.swift b/EhPanda/View/Home/History/HistoryView.swift index 25e7e72f..775e0359 100644 --- a/EhPanda/View/Home/History/HistoryView.swift +++ b/EhPanda/View/Home/History/HistoryView.swift @@ -9,8 +9,7 @@ import SwiftUI import ComposableArchitecture struct HistoryView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double @@ -21,7 +20,6 @@ struct HistoryView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -31,22 +29,22 @@ struct HistoryView: View { var body: some View { let content = GenericList( - galleries: viewStore.filteredGalleries, + galleries: store.filteredGalleries, setting: setting, pageNumber: nil, - loadingState: viewStore.loadingState, + loadingState: store.loadingState, footerLoadingState: .idle, - fetchAction: { viewStore.send(.fetchGalleries) }, - navigateAction: { viewStore.send(.setNavigation(.detail($0))) }, + fetchAction: { store.send(.fetchGalleries) }, + navigateAction: { store.send(.setNavigation(.detail($0))) }, translateAction: { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) - .searchable(text: viewStore.$keyword, prompt: L10n.Localizable.Searchable.Prompt.filter) + .searchable(text: $store.keyword, prompt: L10n.Localizable.Searchable.Prompt.filter) .onAppear { - if viewStore.galleries.isEmpty { + if store.galleries.isEmpty { DispatchQueue.main.async { - viewStore.send(.fetchGalleries) + store.send(.fetchGalleries) } } } @@ -56,10 +54,10 @@ struct HistoryView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: viewStore.$route, case: /HistoryReducer.Route.detail) { route in + .sheet(unwrapping: $store.route, case: /HistoryReducer.Route.detail) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -73,9 +71,9 @@ struct HistoryView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.$route, case: /HistoryReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: /HistoryReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -85,18 +83,18 @@ struct HistoryView: View { private func toolbar() -> some ToolbarContent { CustomToolbarItem { Button { - viewStore.send(.setNavigation(.clearHistory)) + store.send(.setNavigation(.clearHistory)) } label: { Image(systemSymbol: .trashCircle) } - .disabled(viewStore.loadingState != .idle || viewStore.galleries.isEmpty) + .disabled(store.loadingState != .idle || store.galleries.isEmpty) .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.clear, - unwrapping: viewStore.$route, + unwrapping: $store.route, case: /HistoryReducer.Route.clearHistory ) { Button(L10n.Localizable.ConfirmationDialog.Button.clear, role: .destructive) { - viewStore.send(.clearHistoryGalleries) + store.send(.clearHistoryGalleries) } } } diff --git a/EhPanda/View/Home/HomeReducer.swift b/EhPanda/View/Home/HomeReducer.swift index bc26afbd..88b44cff 100644 --- a/EhPanda/View/Home/HomeReducer.swift +++ b/EhPanda/View/Home/HomeReducer.swift @@ -19,10 +19,11 @@ struct HomeReducer { case section(HomeSectionType) } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var cardPageIndex = 1 - @BindingState var currentCardID = "" + var route: Route? + var cardPageIndex = 1 + var currentCardID = "" var allowsCardHitTesting = true var rawCardColors = [String: [Color]]() var cardColors: [Color] { @@ -41,10 +42,10 @@ struct HomeReducer { var popularState = PopularReducer.State() var watchedState = WatchedReducer.State() var historyState = HistoryReducer.State() - @Heap var detailState: DetailReducer.State! + var detailState: Heap init() { - _detailState = .init(.init()) + detailState = .init(.init()) } mutating func setPopularGalleries(_ galleries: [Gallery]) { @@ -97,21 +98,23 @@ struct HomeReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } + .onChange(of: \.cardPageIndex) { _, newValue in + Reduce { state, _ in + guard newValue < state.popularGalleries.count else { return .none } + state.currentCardID = state.popularGalleries[state.cardPageIndex].gid + state.allowsCardHitTesting = false + return .run { send in + try await Task.sleep(for: .milliseconds(300)) + await send(.setAllowsCardHitTesting(true)) + } + } + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - - case .binding(\.$cardPageIndex): - guard state.cardPageIndex < state.popularGalleries.count else { return .none } - state.currentCardID = state.popularGalleries[state.cardPageIndex].gid - state.allowsCardHitTesting = false - return .run { send in - try await Task.sleep(for: .milliseconds(300)) - await send(.setAllowsCardHitTesting(true)) - } - case .binding: return .none @@ -125,7 +128,7 @@ struct HomeReducer { state.popularState = .init() state.watchedState = .init() state.historyState = .init() - state.detailState = .init() + state.detailState.wrappedValue = .init() return .merge( .send(.frontpage(.teardown)), .send(.toplists(.teardown)), @@ -266,6 +269,6 @@ struct HomeReducer { Scope(state: \.popularState, action: /Action.popular, child: PopularReducer.init) Scope(state: \.watchedState, action: /Action.watched, child: WatchedReducer.init) Scope(state: \.historyState, action: /Action.history, child: HistoryReducer.init) - Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Home/HomeView.swift b/EhPanda/View/Home/HomeView.swift index deda931a..1bf6eceb 100644 --- a/EhPanda/View/Home/HomeView.swift +++ b/EhPanda/View/Home/HomeView.swift @@ -12,8 +12,7 @@ import SFSafeSymbols import ComposableArchitecture struct HomeView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double @@ -24,7 +23,6 @@ struct HomeView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -38,60 +36,60 @@ struct HomeView: View { ZStack { ScrollView(showsIndicators: false) { VStack { - if !viewStore.popularGalleries.isEmpty { + if !store.popularGalleries.isEmpty { CardSlideSection( - galleries: viewStore.popularGalleries, - pageIndex: viewStore.$cardPageIndex, - currentID: viewStore.currentCardID, - colors: viewStore.cardColors, + galleries: store.popularGalleries, + pageIndex: $store.cardPageIndex, + currentID: store.currentCardID, + colors: store.cardColors, navigateAction: navigateTo(gid:), webImageSuccessAction: { gid, result in - viewStore.send(.analyzeImageColors(gid, result)) + store.send(.analyzeImageColors(gid, result)) } ) - .equatable().allowsHitTesting(viewStore.allowsCardHitTesting) + .equatable().allowsHitTesting(store.allowsCardHitTesting) } Group { - if viewStore.frontpageGalleries.count > 1 { + if store.frontpageGalleries.count > 1 { CoverWallSection( - galleries: viewStore.frontpageGalleries, - isLoading: viewStore.frontpageLoadingState == .loading, + galleries: store.frontpageGalleries, + isLoading: store.frontpageLoadingState == .loading, navigateAction: navigateTo(gid:), - showAllAction: { viewStore.send(.setNavigation(.section(.frontpage))) }, - reloadAction: { viewStore.send(.fetchFrontpageGalleries) } + showAllAction: { store.send(.setNavigation(.section(.frontpage))) }, + reloadAction: { store.send(.fetchFrontpageGalleries) } ) } ToplistsSection( - galleries: viewStore.toplistsGalleries, - isLoading: !viewStore.toplistsLoadingState + galleries: store.toplistsGalleries, + isLoading: !store.toplistsLoadingState .values.allSatisfy({ $0 != .loading }), navigateAction: navigateTo(gid:), - showAllAction: { viewStore.send(.setNavigation(.section(.toplists))) }, - reloadAction: { viewStore.send(.fetchAllToplistsGalleries) } + showAllAction: { store.send(.setNavigation(.section(.toplists))) }, + reloadAction: { store.send(.fetchAllToplistsGalleries) } ) MiscGridSection(navigateAction: navigateTo(type:)) } .padding(.vertical) } } - .opacity(viewStore.popularGalleries.isEmpty ? 0 : 1).zIndex(2) + .opacity(store.popularGalleries.isEmpty ? 0 : 1).zIndex(2) LoadingView() .opacity( - viewStore.popularLoadingState == .loading - && viewStore.popularGalleries.isEmpty ? 1 : 0 + store.popularLoadingState == .loading + && store.popularGalleries.isEmpty ? 1 : 0 ) .zIndex(0) - let error = (/LoadingState.failed).extract(from: viewStore.popularLoadingState) + let error = (/LoadingState.failed).extract(from: store.popularLoadingState) ErrorView(error: error ?? .unknown) { - viewStore.send(.fetchAllGalleries) + store.send(.fetchAllGalleries) } - .opacity(viewStore.popularGalleries.isEmpty && error != nil ? 1 : 0) + .opacity(store.popularGalleries.isEmpty && error != nil ? 1 : 0) .zIndex(1) } - .animation(.default, value: viewStore.popularLoadingState) + .animation(.default, value: store.popularLoadingState) .onAppear { - if viewStore.popularGalleries.isEmpty { - viewStore.send(.fetchAllGalleries) + if store.popularGalleries.isEmpty { + store.send(.fetchAllGalleries) } } .background(navigationLinks) @@ -100,10 +98,10 @@ struct HomeView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: viewStore.$route, case: /HomeReducer.Route.detail) { route in + .sheet(unwrapping: $store.route, case: /HomeReducer.Route.detail) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -119,12 +117,12 @@ struct HomeView: View { private func toolbar() -> some ToolbarContent { CustomToolbarItem(tint: .primary) { Button { - viewStore.send(.fetchAllGalleries) + store.send(.fetchAllGalleries) } label: { Image(systemSymbol: .arrowCounterclockwise) } - .opacity(viewStore.popularLoadingState == .loading ? 0 : 1) - .overlay(ProgressView().opacity(viewStore.popularLoadingState == .loading ? 1 : 0)) + .opacity(store.popularLoadingState == .loading ? 0 : 1) + .overlay(ProgressView().opacity(store.popularLoadingState == .loading ? 1 : 0)) } } } @@ -139,16 +137,16 @@ private extension HomeView { sectionLink } var detailViewLink: some View { - NavigationLink(unwrapping: viewStore.$route, case: /HomeReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: /HomeReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } } var miscGridLink: some View { - NavigationLink(unwrapping: viewStore.$route, case: /HomeReducer.Route.misc) { route in + NavigationLink(unwrapping: $store.route, case: /HomeReducer.Route.misc) { route in switch route.wrappedValue { case .popular: PopularView( @@ -169,7 +167,7 @@ private extension HomeView { } } var sectionLink: some View { - NavigationLink(unwrapping: viewStore.$route, case: /HomeReducer.Route.section) { route in + NavigationLink(unwrapping: $store.route, case: /HomeReducer.Route.section) { route in switch route.wrappedValue { case .frontpage: FrontpageView( @@ -185,10 +183,10 @@ private extension HomeView { } } func navigateTo(gid: String) { - viewStore.send(.setNavigation(.detail(gid))) + store.send(.setNavigation(.detail(gid))) } func navigateTo(type: HomeMiscGridType) { - viewStore.send(.setNavigation(.misc(type))) + store.send(.setNavigation(.misc(type))) } } diff --git a/EhPanda/View/Home/Popular/PopularReducer.swift b/EhPanda/View/Home/Popular/PopularReducer.swift index feb01187..b1444906 100644 --- a/EhPanda/View/Home/Popular/PopularReducer.swift +++ b/EhPanda/View/Home/Popular/PopularReducer.swift @@ -19,9 +19,10 @@ struct PopularReducer { case fetchGalleries } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var keyword = "" + var route: Route? + var keyword = "" var filteredGalleries: [Gallery] { guard !keyword.isEmpty else { return galleries } @@ -31,10 +32,10 @@ struct PopularReducer { var loadingState: LoadingState = .idle var filtersState = FiltersReducer.State() - @Heap var detailState: DetailReducer.State! + var detailState: Heap init() { - _detailState = .init(.init()) + detailState = .init(.init()) } } @@ -56,12 +57,12 @@ struct PopularReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - case .binding: return .none @@ -70,7 +71,7 @@ struct PopularReducer { return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: - state.detailState = .init() + state.detailState.wrappedValue = .init() state.filtersState = .init() return .send(.detail(.teardown)) @@ -116,6 +117,6 @@ struct PopularReducer { ) Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) - Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Home/Popular/PopularView.swift b/EhPanda/View/Home/Popular/PopularView.swift index 67dce9f7..4044af41 100644 --- a/EhPanda/View/Home/Popular/PopularView.swift +++ b/EhPanda/View/Home/Popular/PopularView.swift @@ -9,8 +9,7 @@ import SwiftUI import ComposableArchitecture struct PopularView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double @@ -21,7 +20,6 @@ struct PopularView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -31,25 +29,25 @@ struct PopularView: View { var body: some View { let content = GenericList( - galleries: viewStore.filteredGalleries, + galleries: store.filteredGalleries, setting: setting, pageNumber: nil, - loadingState: viewStore.loadingState, + loadingState: store.loadingState, footerLoadingState: .idle, - fetchAction: { viewStore.send(.fetchGalleries) }, - navigateAction: { viewStore.send(.setNavigation(.detail($0))) }, + fetchAction: { store.send(.fetchGalleries) }, + navigateAction: { store.send(.setNavigation(.detail($0))) }, translateAction: { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) - .sheet(unwrapping: viewStore.$route, case: /PopularReducer.Route.filters) { _ in + .sheet(unwrapping: $store.route, case: /PopularReducer.Route.filters) { _ in FiltersView(store: store.scope(state: \.filtersState, action: \.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } - .searchable(text: viewStore.$keyword, prompt: L10n.Localizable.Searchable.Prompt.filter) + .searchable(text: $store.keyword, prompt: L10n.Localizable.Searchable.Prompt.filter) .onAppear { - if viewStore.galleries.isEmpty { + if store.galleries.isEmpty { DispatchQueue.main.async { - viewStore.send(.fetchGalleries) + store.send(.fetchGalleries) } } } @@ -59,10 +57,10 @@ struct PopularView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: viewStore.$route, case: /PopularReducer.Route.detail) { route in + .sheet(unwrapping: $store.route, case: /PopularReducer.Route.detail) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -76,9 +74,9 @@ struct PopularView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.$route, case: /PopularReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: /PopularReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -88,7 +86,7 @@ struct PopularView: View { private func toolbar() -> some ToolbarContent { CustomToolbarItem { FiltersButton(hideText: true) { - viewStore.send(.setNavigation(.filters)) + store.send(.setNavigation(.filters)) } } } diff --git a/EhPanda/View/Home/Toplists/ToplistsReducer.swift b/EhPanda/View/Home/Toplists/ToplistsReducer.swift index 28e3046b..c48cf9ed 100644 --- a/EhPanda/View/Home/Toplists/ToplistsReducer.swift +++ b/EhPanda/View/Home/Toplists/ToplistsReducer.swift @@ -18,12 +18,13 @@ struct ToplistsReducer { case fetchGalleries, fetchMoreGalleries } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var keyword = "" - @BindingState var jumpPageIndex = "" - @BindingState var jumpPageAlertFocused = false - @BindingState var jumpPageAlertPresented = false + var route: Route? + var keyword = "" + var jumpPageIndex = "" + var jumpPageAlertFocused = false + var jumpPageAlertPresented = false var type: ToplistsType = .yesterday @@ -50,10 +51,10 @@ struct ToplistsReducer { rawFooterLoadingState[type] } - @Heap var detailState: DetailReducer.State! + var detailState: Heap init() { - _detailState = .init(.init()) + detailState = .init(.init()) } mutating func insertGalleries(type: ToplistsType, galleries: [Gallery]) { @@ -89,18 +90,20 @@ struct ToplistsReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } + .onChange(of: \.jumpPageAlertPresented) { _, newValue in + Reduce { state, _ in + if !newValue { + state.jumpPageAlertFocused = false + } + return .none + } + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - - case .binding(\.$jumpPageAlertPresented): - if !state.jumpPageAlertPresented { - state.jumpPageAlertFocused = false - } - return .none - case .binding: return .none @@ -114,7 +117,7 @@ struct ToplistsReducer { return .send(.fetchGalleries()) case .clearSubStates: - state.detailState = .init() + state.detailState.wrappedValue = .init() return .send(.detail(.teardown)) case .performJumpPage: @@ -213,6 +216,6 @@ struct ToplistsReducer { } } - Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Home/Toplists/ToplistsView.swift b/EhPanda/View/Home/Toplists/ToplistsView.swift index a7f6236c..1772a39b 100644 --- a/EhPanda/View/Home/Toplists/ToplistsView.swift +++ b/EhPanda/View/Home/Toplists/ToplistsView.swift @@ -9,8 +9,7 @@ import SwiftUI import ComposableArchitecture struct ToplistsView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double @@ -21,7 +20,6 @@ struct ToplistsView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -29,38 +27,38 @@ struct ToplistsView: View { } private var navigationTitle: String { - [L10n.Localizable.ToplistsView.Title.toplists, viewStore.type.value].joined(separator: " - ") + [L10n.Localizable.ToplistsView.Title.toplists, store.type.value].joined(separator: " - ") } var body: some View { let content = GenericList( - galleries: viewStore.filteredGalleries ?? [], + galleries: store.filteredGalleries ?? [], setting: setting, - pageNumber: viewStore.pageNumber, - loadingState: viewStore.loadingState ?? .idle, - footerLoadingState: viewStore.footerLoadingState ?? .idle, - fetchAction: { viewStore.send(.fetchGalleries()) }, - fetchMoreAction: { viewStore.send(.fetchMoreGalleries) }, - navigateAction: { viewStore.send(.setNavigation(.detail($0))) }, + pageNumber: store.pageNumber, + loadingState: store.loadingState ?? .idle, + footerLoadingState: store.footerLoadingState ?? .idle, + fetchAction: { store.send(.fetchGalleries()) }, + fetchMoreAction: { store.send(.fetchMoreGalleries) }, + navigateAction: { store.send(.setNavigation(.detail($0))) }, translateAction: { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) .jumpPageAlert( - index: viewStore.$jumpPageIndex, - isPresented: viewStore.$jumpPageAlertPresented, - isFocused: viewStore.$jumpPageAlertFocused, - pageNumber: viewStore.pageNumber ?? .init(), - jumpAction: { viewStore.send(.performJumpPage) } + index: $store.jumpPageIndex, + isPresented: $store.jumpPageAlertPresented, + isFocused: $store.jumpPageAlertFocused, + pageNumber: store.pageNumber ?? .init(), + jumpAction: { store.send(.performJumpPage) } ) - .searchable(text: viewStore.$keyword, prompt: L10n.Localizable.Searchable.Prompt.filter) - .navigationBarBackButtonHidden(viewStore.jumpPageAlertPresented) - .animation(.default, value: viewStore.jumpPageAlertPresented) + .searchable(text: $store.keyword, prompt: L10n.Localizable.Searchable.Prompt.filter) + .navigationBarBackButtonHidden(store.jumpPageAlertPresented) + .animation(.default, value: store.jumpPageAlertPresented) .onAppear { - if viewStore.galleries?.isEmpty != false { + if store.galleries?.isEmpty != false { DispatchQueue.main.async { - viewStore.send(.fetchGalleries()) + store.send(.fetchGalleries()) } } } @@ -70,10 +68,10 @@ struct ToplistsView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: viewStore.$route, case: /ToplistsReducer.Route.detail) { route in + .sheet(unwrapping: $store.route, case: /ToplistsReducer.Route.detail) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -87,9 +85,9 @@ struct ToplistsView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.$route, case: /ToplistsReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: /ToplistsReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -97,17 +95,17 @@ struct ToplistsView: View { } } private func toolbar() -> some ToolbarContent { - CustomToolbarItem(disabled: viewStore.jumpPageAlertPresented) { - ToplistsTypeMenu(type: viewStore.type) { type in - if type != viewStore.type { - viewStore.send(.setToplistsType(type)) + CustomToolbarItem(disabled: store.jumpPageAlertPresented) { + ToplistsTypeMenu(type: store.type) { type in + if type != store.type { + store.send(.setToplistsType(type)) } } if AppUtil.galleryHost == .ehentai { - JumpPageButton(pageNumber: viewStore.pageNumber ?? .init(), hideText: true) { - viewStore.send(.presentJumpPageAlert) + JumpPageButton(pageNumber: store.pageNumber ?? .init(), hideText: true) { + store.send(.presentJumpPageAlert) DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - viewStore.send(.setJumpPageAlertFocused(true)) + store.send(.setJumpPageAlertFocused(true)) } } } diff --git a/EhPanda/View/Home/Watched/WatchedReducer.swift b/EhPanda/View/Home/Watched/WatchedReducer.swift index eae05e1e..1955541d 100644 --- a/EhPanda/View/Home/Watched/WatchedReducer.swift +++ b/EhPanda/View/Home/Watched/WatchedReducer.swift @@ -20,9 +20,10 @@ struct WatchedReducer { case fetchGalleries, fetchMoreGalleries } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var keyword = "" + var route: Route? + var keyword = "" var galleries = [Gallery]() var pageNumber = PageNumber() @@ -31,10 +32,10 @@ struct WatchedReducer { var filtersState = FiltersReducer.State() var quickSearchState = QuickSearchReducer.State() - @Heap var detailState: DetailReducer.State! + var detailState: Heap init() { - _detailState = .init(.init()) + detailState = .init(.init()) } mutating func insertGalleries(_ galleries: [Gallery]) { @@ -68,12 +69,12 @@ struct WatchedReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - case .binding: return .none @@ -82,7 +83,7 @@ struct WatchedReducer { return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: - state.detailState = .init() + state.detailState.wrappedValue = .init() state.filtersState = .init() state.quickSearchState = .init() return .merge( @@ -189,6 +190,6 @@ struct WatchedReducer { Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) - Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Home/Watched/WatchedView.swift b/EhPanda/View/Home/Watched/WatchedView.swift index 0f0f21ce..7b249503 100644 --- a/EhPanda/View/Home/Watched/WatchedView.swift +++ b/EhPanda/View/Home/Watched/WatchedView.swift @@ -9,8 +9,7 @@ import SwiftUI import ComposableArchitecture struct WatchedView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double @@ -21,7 +20,6 @@ struct WatchedView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -33,50 +31,50 @@ struct WatchedView: View { ZStack { if CookieUtil.didLogin { GenericList( - galleries: viewStore.galleries, + galleries: store.galleries, setting: setting, - pageNumber: viewStore.pageNumber, - loadingState: viewStore.loadingState, - footerLoadingState: viewStore.footerLoadingState, - fetchAction: { viewStore.send(.fetchGalleries()) }, - fetchMoreAction: { viewStore.send(.fetchMoreGalleries) }, - navigateAction: { viewStore.send(.setNavigation(.detail($0))) }, + pageNumber: store.pageNumber, + loadingState: store.loadingState, + footerLoadingState: store.footerLoadingState, + fetchAction: { store.send(.fetchGalleries()) }, + fetchMoreAction: { store.send(.fetchMoreGalleries) }, + navigateAction: { store.send(.setNavigation(.detail($0))) }, translateAction: { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) } else { - NotLoginView(action: { viewStore.send(.onNotLoginViewButtonTapped) }) + NotLoginView(action: { store.send(.onNotLoginViewButtonTapped) }) } } - .sheet(unwrapping: viewStore.$route, case: /WatchedReducer.Route.quickSearch) { _ in + .sheet(unwrapping: $store.route, case: /WatchedReducer.Route.quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickSearchState, action: \.quickSearch) ) { keyword in - viewStore.send(.setNavigation(nil)) - viewStore.send(.fetchGalleries(keyword)) + store.send(.setNavigation(nil)) + store.send(.fetchGalleries(keyword)) } .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.$route, case: /WatchedReducer.Route.filters) { _ in + .sheet(unwrapping: $store.route, case: /WatchedReducer.Route.filters) { _ in FiltersView(store: store.scope(state: \.filtersState, action: \.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } - .searchable(text: viewStore.$keyword) + .searchable(text: $store.keyword) .searchSuggestions { TagSuggestionView( - keyword: viewStore.$keyword, translations: tagTranslator.translations, + keyword: $store.keyword, translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion ) } .onSubmit(of: .search) { - viewStore.send(.fetchGalleries()) + store.send(.fetchGalleries()) } .onAppear { - if viewStore.galleries.isEmpty && CookieUtil.didLogin { + if store.galleries.isEmpty && CookieUtil.didLogin { DispatchQueue.main.async { - viewStore.send(.fetchGalleries()) + store.send(.fetchGalleries()) } } } @@ -86,10 +84,10 @@ struct WatchedView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: viewStore.$route, case: /WatchedReducer.Route.detail) { route in + .sheet(unwrapping: $store.route, case: /WatchedReducer.Route.detail) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -103,9 +101,9 @@ struct WatchedView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.$route, case: /WatchedReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: /WatchedReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -116,10 +114,10 @@ struct WatchedView: View { CustomToolbarItem { ToolbarFeaturesMenu { FiltersButton { - viewStore.send(.setNavigation(.filters)) + store.send(.setNavigation(.filters)) } QuickSearchButton { - viewStore.send(.setNavigation(.quickSearch)) + store.send(.setNavigation(.quickSearch)) } } } diff --git a/EhPanda/View/Migration/MigrationReducer.swift b/EhPanda/View/Migration/MigrationReducer.swift index af26b5eb..83c3bf6b 100644 --- a/EhPanda/View/Migration/MigrationReducer.swift +++ b/EhPanda/View/Migration/MigrationReducer.swift @@ -15,8 +15,9 @@ struct MigrationReducer { case dropDialog } + @ObservableState struct State: Equatable { - @BindingState var route: Route? + var route: Route? var databaseState: LoadingState = .loading } diff --git a/EhPanda/View/Migration/MigrationView.swift b/EhPanda/View/Migration/MigrationView.swift index ed316acf..c046e0e0 100644 --- a/EhPanda/View/Migration/MigrationView.swift +++ b/EhPanda/View/Migration/MigrationView.swift @@ -10,8 +10,7 @@ import ComposableArchitecture struct MigrationView: View { @Environment(\.colorScheme) private var colorScheme - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private var reversedPrimary: Color { colorScheme == .light ? .white : .black @@ -19,7 +18,6 @@ struct MigrationView: View { init(store: StoreOf) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) } var body: some View { @@ -27,26 +25,26 @@ struct MigrationView: View { ZStack { reversedPrimary.ignoresSafeArea() LoadingView(title: L10n.Localizable.LoadingView.Title.preparingDatabase) - .opacity(viewStore.databaseState == .loading ? 1 : 0) - let error = (/LoadingState.failed).extract(from: viewStore.databaseState) + .opacity(store.databaseState == .loading ? 1 : 0) + let error = (/LoadingState.failed).extract(from: store.databaseState) let errorNonNil = error ?? .databaseCorrupted(nil) AlertView(symbol: errorNonNil.symbol, message: errorNonNil.localizedDescription) { AlertViewButton(title: L10n.Localizable.ErrorView.Button.dropDatabase) { - viewStore.send(.setNavigation(.dropDialog)) + store.send(.setNavigation(.dropDialog)) } .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.dropDatabase, - unwrapping: viewStore.$route, + unwrapping: $store.route, case: /MigrationReducer.Route.dropDialog ) { Button(L10n.Localizable.ConfirmationDialog.Button.dropDatabase, role: .destructive) { - viewStore.send(.dropDatabase) + store.send(.dropDatabase) } } } .opacity(error != nil ? 1 : 0) } - .animation(.default, value: viewStore.databaseState) + .animation(.default, value: store.databaseState) } } } diff --git a/EhPanda/View/Reading/ReadingReducer.swift b/EhPanda/View/Reading/ReadingReducer.swift index cdaf3c30..b2f5f4ea 100644 --- a/EhPanda/View/Reading/ReadingReducer.swift +++ b/EhPanda/View/Reading/ReadingReducer.swift @@ -48,8 +48,9 @@ struct ReadingReducer { case fetchMPVImageURL } + @ObservableState struct State: Equatable { - @BindingState var route: Route? + var route: Route? var gallery: Gallery = .empty var galleryDetail: GalleryDetail? @@ -73,8 +74,8 @@ struct ReadingReducer { var mpvImageKeys = [Int: String]() var mpvSkipServerIdentifiers = [Int: String]() - @BindingState var showsPanel = false - @BindingState var showsSliderPreview = false + var showsPanel = false + var showsSliderPreview = false // Update func update(stored: inout [Int: T], new: [Int: T], replaceExisting: Bool = true) { @@ -188,12 +189,12 @@ struct ReadingReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.showsSliderPreview) { _, _ in + Reduce({ _, _ in .run(operation: { _ in hapticsClient.generateFeedback(.soft) }) }) + } Reduce { state, action in switch action { - case .binding(\.$showsSliderPreview): - return .run(operation: { _ in hapticsClient.generateFeedback(.soft) }) - case .binding: return .none diff --git a/EhPanda/View/Reading/ReadingView.swift b/EhPanda/View/Reading/ReadingView.swift index de323c7b..88b289dd 100644 --- a/EhPanda/View/Reading/ReadingView.swift +++ b/EhPanda/View/Reading/ReadingView.swift @@ -13,8 +13,7 @@ import ComposableArchitecture struct ReadingView: View { @Environment(\.colorScheme) private var colorScheme - let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable var store: StoreOf private let gid: String @Binding private var setting: Setting private let blurRadius: Double @@ -30,7 +29,6 @@ struct ReadingView: View { gid: String, setting: Binding, blurRadius: Double ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.gid = gid _setting = setting self.blurRadius = blurRadius @@ -46,7 +44,7 @@ struct ReadingView: View { ZStack { if setting.readingDirection == .vertical { AdvancedList( - page: page, data: viewStore.state.containerDataSource(setting: setting), + page: page, data: store.state.containerDataSource(setting: setting), id: \.self, spacing: setting.contentDividerHeight, gesture: SimultaneousGesture(magnificationGesture, tapGesture), content: imageStack @@ -54,7 +52,7 @@ struct ReadingView: View { .disabled(gestureHandler.scale != 1) } else { Pager( - page: page, data: viewStore.state.containerDataSource(setting: setting), + page: page, data: store.state.containerDataSource(setting: setting), id: \.self, content: imageStack ) .horizontal(setting.readingDirection == .rightToLeft ? .endToStart : .startToEnd) @@ -64,24 +62,24 @@ struct ReadingView: View { .scaleEffect(gestureHandler.scale, anchor: gestureHandler.scaleAnchor) .offset(gestureHandler.offset).gesture(tapGesture).gesture(dragGesture) .gesture(magnificationGesture).ignoresSafeArea() - .id(viewStore.databaseLoadingState) - .id(viewStore.forceRefreshID) + .id(store.databaseLoadingState) + .id(store.forceRefreshID) ControlPanel( - showsPanel: viewStore.$showsPanel, - showsSliderPreview: viewStore.$showsSliderPreview, + showsPanel: $store.showsPanel, + showsSliderPreview: $store.showsSliderPreview, sliderValue: $pageHandler.sliderValue, setting: $setting, enablesLiveText: $liveTextHandler.enablesLiveText, autoPlayPolicy: .init(get: { autoPlayHandler.policy }, set: { setAutoPlayPolocy($0) }), - range: 1...Float(viewStore.gallery.pageCount), previewURLs: viewStore.previewURLs, + range: 1...Float(store.gallery.pageCount), previewURLs: store.previewURLs, dismissGesture: controlPanelDismissGesture, - dismissAction: { viewStore.send(.onPerformDismiss) }, - navigateSettingAction: { viewStore.send(.setNavigation(.readingSetting)) }, - reloadAllImagesAction: { viewStore.send(.reloadAllWebImages) }, - retryAllFailedImagesAction: { viewStore.send(.retryAllFailedWebImages) }, - fetchPreviewURLsAction: { viewStore.send(.fetchPreviewURLs($0)) } + dismissAction: { store.send(.onPerformDismiss) }, + navigateSettingAction: { store.send(.setNavigation(.readingSetting)) }, + reloadAllImagesAction: { store.send(.reloadAllWebImages) }, + retryAllFailedImagesAction: { store.send(.retryAllFailedWebImages) }, + fetchPreviewURLsAction: { store.send(.fetchPreviewURLs($0)) } ) } - .sheet(unwrapping: viewStore.$route, case: /ReadingReducer.Route.readingSetting) { _ in + .sheet(unwrapping: $store.route, case: /ReadingReducer.Route.readingSetting) { _ in NavigationView { ReadingSettingView( readingDirection: $setting.readingDirection, @@ -95,7 +93,7 @@ struct ReadingView: View { CustomToolbarItem(placement: .cancellationAction) { if !DeviceUtil.isPad && DeviceUtil.isLandscape { Button { - viewStore.send(.setNavigation(nil)) + store.send(.setNavigation(nil)) } label: { Image(systemSymbol: .chevronDown) } @@ -106,13 +104,13 @@ struct ReadingView: View { .accentColor(setting.accentColor).tint(setting.accentColor) .autoBlur(radius: blurRadius).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.$route, case: /ReadingReducer.Route.share) { route in + .sheet(unwrapping: $store.route, case: /ReadingReducer.Route.share) { route in ActivityView(activityItems: [route.wrappedValue.associatedValue]) .accentColor(setting.accentColor).autoBlur(radius: blurRadius) } .progressHUD( - config: viewStore.hudConfig, - unwrapping: viewStore.$route, + config: store.hudConfig, + unwrapping: $store.route, case: /ReadingReducer.Route.hud ) @@ -120,32 +118,32 @@ struct ReadingView: View { .onChange(of: page.index) { _, newValue in Logger.info("page.index changed", context: ["pageIndex": newValue]) let newValue = pageHandler.mapFromPager( - index: newValue, pageCount: viewStore.gallery.pageCount, setting: setting + index: newValue, pageCount: store.gallery.pageCount, setting: setting ) pageHandler.sliderValue = .init(newValue) - if viewStore.databaseLoadingState == .idle { - viewStore.send(.syncReadingProgress(.init(newValue))) + if store.databaseLoadingState == .idle { + store.send(.syncReadingProgress(.init(newValue))) } } .onChange(of: pageHandler.sliderValue) { _, newValue in Logger.info("pageHandler.sliderValue changed", context: ["sliderValue": newValue]) - if !viewStore.showsSliderPreview { + if !store.showsSliderPreview { setPageIndex(sliderValue: newValue) } } - .onChange(of: viewStore.showsSliderPreview) { _, newValue in - Logger.info("viewStore.showsSliderPreview changed", context: ["isShown": newValue]) + .onChange(of: store.showsSliderPreview) { _, newValue in + Logger.info("store.showsSliderPreview changed", context: ["isShown": newValue]) if !newValue { setPageIndex(sliderValue: pageHandler.sliderValue) } setAutoPlayPolocy(.off) } - .onChange(of: viewStore.readingProgress) { _, newValue in - Logger.info("viewStore.readingProgress changed", context: ["readingProgress": newValue]) + .onChange(of: store.readingProgress) { _, newValue in + Logger.info("store.readingProgress changed", context: ["readingProgress": newValue]) pageHandler.sliderValue = .init(newValue) } // AutoPlay - .onChange(of: viewStore.route) { _, newValue in - Logger.info("viewStore.route changed", context: ["route": newValue]) + .onChange(of: store.route) { _, newValue in + Logger.info("store.route changed", context: ["route": newValue]) if ![.hud, .none].contains(newValue) { setAutoPlayPolocy(.off) } @@ -154,11 +152,11 @@ struct ReadingView: View { // LiveText .onChange(of: liveTextHandler.enablesLiveText) { _, newValue in Logger.info("liveTextHandler.enablesLiveText changed", context: ["isEnabled": newValue]) - if newValue { viewStore.webImageLoadSuccessIndices.forEach(analyzeImageForLiveText) } + if newValue { store.webImageLoadSuccessIndices.forEach(analyzeImageForLiveText) } } - .onChange(of: viewStore.webImageLoadSuccessIndices) { _, newValue in - Logger.info("viewStore.webImageLoadSuccessIndices changed", context: [ - "count": viewStore.webImageLoadSuccessIndices.count + .onChange(of: store.webImageLoadSuccessIndices) { _, newValue in + Logger.info("store.webImageLoadSuccessIndices changed", context: [ + "count": store.webImageLoadSuccessIndices.count ]) if liveTextHandler.enablesLiveText { newValue.forEach(analyzeImageForLiveText) @@ -168,41 +166,41 @@ struct ReadingView: View { // Orientation .onChange(of: setting.enablesLandscape) { _, newValue in Logger.info("setting.enablesLandscape changed", context: ["newValue": newValue]) - viewStore.send(.setOrientationPortrait(!newValue)) + store.send(.setOrientationPortrait(!newValue)) } .animation(.linear(duration: 0.1), value: gestureHandler.offset) .animation(.default, value: liveTextHandler.enablesLiveText) .animation(.default, value: liveTextHandler.liveTextGroups) .animation(.default, value: gestureHandler.scale) - .animation(.default, value: viewStore.showsPanel) - .statusBar(hidden: !viewStore.showsPanel) + .animation(.default, value: store.showsPanel) + .statusBar(hidden: !store.showsPanel) .onDisappear { liveTextHandler.cancelRequests() setAutoPlayPolocy(.off) } - .onAppear { viewStore.send(.onAppear(gid, setting.enablesLandscape)) } + .onAppear { store.send(.onAppear(gid, setting.enablesLandscape)) } } @ViewBuilder private func imageStack(index: Int) -> some View { - let imageStackConfig = viewStore.state.imageContainerConfigs(index: index, setting: setting) + let imageStackConfig = store.state.imageContainerConfigs(index: index, setting: setting) let isDualPage = setting.enablesDualPageMode && setting.readingDirection != .vertical && DeviceUtil.isLandscape HorizontalImageStack( - index: index, isDualPage: isDualPage, isDatabaseLoading: viewStore.databaseLoadingState != .idle, - backgroundColor: backgroundColor, config: imageStackConfig, imageURLs: viewStore.imageURLs, - originalImageURLs: viewStore.originalImageURLs, loadingStates: viewStore.imageURLLoadingStates, + index: index, isDualPage: isDualPage, isDatabaseLoading: store.databaseLoadingState != .idle, + backgroundColor: backgroundColor, config: imageStackConfig, imageURLs: store.imageURLs, + originalImageURLs: store.originalImageURLs, loadingStates: store.imageURLLoadingStates, enablesLiveText: liveTextHandler.enablesLiveText, liveTextGroups: liveTextHandler.liveTextGroups, focusedLiveTextGroup: liveTextHandler.focusedLiveTextGroup, liveTextTapAction: liveTextHandler.setFocusedLiveTextGroup, - fetchAction: { viewStore.send(.fetchImageURLs($0)) }, - refetchAction: { viewStore.send(.refetchImageURLs($0)) }, - prefetchAction: { viewStore.send(.prefetchImages($0, setting.prefetchLimit)) }, - loadRetryAction: { viewStore.send(.onWebImageRetry($0)) }, - loadSucceededAction: { viewStore.send(.onWebImageSucceeded($0)) }, - loadFailedAction: { viewStore.send(.onWebImageFailed($0)) }, - copyImageAction: { viewStore.send(.copyImage($0)) }, - saveImageAction: { viewStore.send(.saveImage($0)) }, - shareImageAction: { viewStore.send(.shareImage($0)) } + fetchAction: { store.send(.fetchImageURLs($0)) }, + refetchAction: { store.send(.refetchImageURLs($0)) }, + prefetchAction: { store.send(.prefetchImages($0, setting.prefetchLimit)) }, + loadRetryAction: { store.send(.onWebImageRetry($0)) }, + loadSucceededAction: { store.send(.onWebImageSucceeded($0)) }, + loadFailedAction: { store.send(.onWebImageFailed($0)) }, + copyImageAction: { store.send(.copyImage($0)) }, + saveImageAction: { store.send(.saveImage($0)) }, + shareImageAction: { store.send(.shareImage($0)) } ) } } @@ -230,7 +228,7 @@ extension ReadingView { Logger.info("analyzeImageForLiveText duplicated", context: ["index": index]) return } - guard let key = viewStore.imageURLs[index]?.absoluteString else { + guard let key = store.imageURLs[index]?.absoluteString else { Logger.info("analyzeImageForLiveText URL not found", context: ["index": index]) return } @@ -240,7 +238,7 @@ extension ReadingView { if let image = result.image, let cgImage = image.cgImage { liveTextHandler.analyzeImage( cgImage, size: image.size, index: index, recognitionLanguages: - viewStore.galleryDetail?.language.codes + store.galleryDetail?.language.codes ) } else { Logger.info("analyzeImageForLiveText image not found", context: ["index": index]) @@ -271,7 +269,7 @@ extension ReadingView { page.update(.new(index: newValue)) Logger.info("Pager.update", context: ["update": newValue]) }, - toggleShowsPanelAction: { viewStore.send(.toggleShowsPanel) } + toggleShowsPanelAction: { store.send(.toggleShowsPanel) } ) } let doubleTap = TapGesture(count: 2) @@ -304,7 +302,7 @@ extension ReadingView { var controlPanelDismissGesture: some Gesture { DragGesture().onEnded { gestureHandler.onControlPanelDismissGestureEnded( - value: $0, dismissAction: { viewStore.send(.onPerformDismiss) } + value: $0, dismissAction: { store.send(.onPerformDismiss) } ) } } diff --git a/EhPanda/View/Search/SearchReducer.swift b/EhPanda/View/Search/SearchReducer.swift index 65c061e7..239727b4 100644 --- a/EhPanda/View/Search/SearchReducer.swift +++ b/EhPanda/View/Search/SearchReducer.swift @@ -20,9 +20,10 @@ struct SearchReducer { case fetchGalleries, fetchMoreGalleries } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var keyword = "" + var route: Route? + var keyword = "" var lastKeyword = "" var galleries = [Gallery]() @@ -31,11 +32,11 @@ struct SearchReducer { var footerLoadingState: LoadingState = .idle var filtersState = FiltersReducer.State() - @Heap var detailState: DetailReducer.State! + var detailState: Heap var quickSearchState = QuickSearchReducer.State() init() { - _detailState = .init(.init()) + detailState = .init(.init()) } mutating func insertGalleries(_ galleries: [Gallery]) { @@ -68,18 +69,20 @@ struct SearchReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } + .onChange(of: \.keyword) { _, newValue in + Reduce { state, _ in + if !newValue.isEmpty { + state.lastKeyword = newValue + } + return .none + } + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - - case .binding(\.$keyword): - if !state.keyword.isEmpty { - state.lastKeyword = state.keyword - } - return .none - case .binding: return .none @@ -88,7 +91,7 @@ struct SearchReducer { return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: - state.detailState = .init() + state.detailState.wrappedValue = .init() state.filtersState = .init() state.quickSearchState = .init() return .merge( @@ -193,6 +196,6 @@ struct SearchReducer { Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) - Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Search/SearchRootReducer.swift b/EhPanda/View/Search/SearchRootReducer.swift index f50ffd7d..92b74755 100644 --- a/EhPanda/View/Search/SearchRootReducer.swift +++ b/EhPanda/View/Search/SearchRootReducer.swift @@ -17,9 +17,10 @@ struct SearchRootReducer { case detail(String) } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var keyword = "" + var route: Route? + var keyword = "" var historyGalleries = [Gallery]() var historyKeywords = [String]() @@ -28,10 +29,10 @@ struct SearchRootReducer { var searchState = SearchReducer.State() var filtersState = FiltersReducer.State() var quickSearchState = QuickSearchReducer.State() - @Heap var detailState: DetailReducer.State! + var detailState: Heap init() { - _detailState = .init(.init()) + detailState = .init(.init()) } mutating func appendHistoryKeywords(_ keywords: [String]) { @@ -90,17 +91,19 @@ struct SearchRootReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce { _, _ in + newValue == nil + ? .merge( + .send(.clearSubStates), + .send(.fetchDatabaseInfos) + ) + : .none + } + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil - ? .merge( - .send(.clearSubStates), - .send(.fetchDatabaseInfos) - ) - : .none - case .binding: return .none @@ -119,7 +122,7 @@ struct SearchRootReducer { case .clearSubStates: state.searchState = .init() - state.detailState = .init() + state.detailState.wrappedValue = .init() state.filtersState = .init() state.quickSearchState = .init() return .merge( @@ -197,6 +200,6 @@ struct SearchRootReducer { Scope(state: \.searchState, action: /Action.search, child: SearchReducer.init) Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) - Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Search/SearchRootView.swift b/EhPanda/View/Search/SearchRootView.swift index 397a7d1e..90bee1ab 100644 --- a/EhPanda/View/Search/SearchRootView.swift +++ b/EhPanda/View/Search/SearchRootView.swift @@ -9,8 +9,7 @@ import SwiftUI import ComposableArchitecture struct SearchRootView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double @@ -21,7 +20,6 @@ struct SearchRootView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -33,48 +31,48 @@ struct SearchRootView: View { let content = ScrollView(showsIndicators: false) { SuggestionsPanel( - historyKeywords: viewStore.historyKeywords.reversed(), - historyGalleries: viewStore.historyGalleries, - quickSearchWords: viewStore.quickSearchWords, - navigateGalleryAction: { viewStore.send(.setNavigation(.detail($0))) }, - navigateQuickSearchAction: { viewStore.send(.setNavigation(.quickSearch)) }, + historyKeywords: store.historyKeywords.reversed(), + historyGalleries: store.historyGalleries, + quickSearchWords: store.quickSearchWords, + navigateGalleryAction: { store.send(.setNavigation(.detail($0))) }, + navigateQuickSearchAction: { store.send(.setNavigation(.quickSearch)) }, searchKeywordAction: { keyword in - viewStore.send(.setKeyword(keyword)) - viewStore.send(.setNavigation(.search)) + store.send(.setKeyword(keyword)) + store.send(.setNavigation(.search)) }, - removeKeywordAction: { viewStore.send(.removeHistoryKeyword($0)) } + removeKeywordAction: { store.send(.removeHistoryKeyword($0)) } ) } - .sheet(unwrapping: viewStore.$route, case: /SearchRootReducer.Route.filters) { _ in + .sheet(unwrapping: $store.route, case: /SearchRootReducer.Route.filters) { _ in FiltersView(store: store.scope(state: \.filtersState, action: \.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } - .sheet(unwrapping: viewStore.$route, case: /SearchRootReducer.Route.quickSearch) { _ in + .sheet(unwrapping: $store.route, case: /SearchRootReducer.Route.quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickSearchState, action: \.quickSearch) ) { keyword in - viewStore.send(.setNavigation(nil)) - viewStore.send(.setKeyword(keyword)) + store.send(.setNavigation(nil)) + store.send(.setKeyword(keyword)) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - viewStore.send(.setNavigation(.search)) + store.send(.setNavigation(.search)) } } .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .searchable(text: viewStore.$keyword) + .searchable(text: $store.keyword) .searchSuggestions { TagSuggestionView( - keyword: viewStore.$keyword, translations: tagTranslator.translations, + keyword: $store.keyword, translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion ) } .onSubmit(of: .search) { - viewStore.send(.setNavigation(.search)) + store.send(.setNavigation(.search)) } .onAppear { - viewStore.send(.fetchHistoryGalleries) - viewStore.send(.fetchDatabaseInfos) + store.send(.fetchHistoryGalleries) + store.send(.fetchDatabaseInfos) } .background(navigationLinks) .toolbar(content: toolbar) @@ -82,10 +80,10 @@ struct SearchRootView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: viewStore.$route, case: /SearchRootReducer.Route.detail) { route in + .sheet(unwrapping: $store.route, case: /SearchRootReducer.Route.detail) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -102,10 +100,10 @@ struct SearchRootView: View { CustomToolbarItem(tint: .primary) { ToolbarFeaturesMenu(symbolRenderingMode: .hierarchical) { FiltersButton { - viewStore.send(.setNavigation(.filters)) + store.send(.setNavigation(.filters)) } QuickSearchButton { - viewStore.send(.setNavigation(.quickSearch)) + store.send(.setNavigation(.quickSearch)) } } } @@ -120,19 +118,19 @@ private extension SearchRootView { searchViewLink } var detailViewLink: some View { - NavigationLink(unwrapping: viewStore.$route, case: /SearchRootReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: /SearchRootReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } } var searchViewLink: some View { - NavigationLink(unwrapping: viewStore.$route, case: /SearchRootReducer.Route.search) { _ in + NavigationLink(unwrapping: $store.route, case: /SearchRootReducer.Route.search) { _ in SearchView( store: store.scope(state: \.searchState, action: \.search), - keyword: viewStore.keyword, user: user, setting: $setting, + keyword: store.keyword, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } diff --git a/EhPanda/View/Search/SearchView.swift b/EhPanda/View/Search/SearchView.swift index 393ddeae..e86ae28b 100644 --- a/EhPanda/View/Search/SearchView.swift +++ b/EhPanda/View/Search/SearchView.swift @@ -9,8 +9,7 @@ import SwiftUI import ComposableArchitecture struct SearchView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let keyword: String private let user: User @Binding private var setting: Setting @@ -22,7 +21,6 @@ struct SearchView: View { keyword: String, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.keyword = keyword self.user = user _setting = setting @@ -33,59 +31,59 @@ struct SearchView: View { var body: some View { let content = GenericList( - galleries: viewStore.galleries, + galleries: store.galleries, setting: setting, - pageNumber: viewStore.pageNumber, - loadingState: viewStore.loadingState, - footerLoadingState: viewStore.footerLoadingState, - fetchAction: { viewStore.send(.fetchGalleries()) }, - fetchMoreAction: { viewStore.send(.fetchMoreGalleries) }, - navigateAction: { viewStore.send(.setNavigation(.detail($0))) }, + pageNumber: store.pageNumber, + loadingState: store.loadingState, + footerLoadingState: store.footerLoadingState, + fetchAction: { store.send(.fetchGalleries()) }, + fetchMoreAction: { store.send(.fetchMoreGalleries) }, + navigateAction: { store.send(.setNavigation(.detail($0))) }, translateAction: { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) - .sheet(unwrapping: viewStore.$route, case: /SearchReducer.Route.quickSearch) { _ in + .sheet(unwrapping: $store.route, case: /SearchReducer.Route.quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickSearchState, action: \.quickSearch) ) { keyword in - viewStore.send(.setNavigation(nil)) - viewStore.send(.fetchGalleries(keyword)) + store.send(.setNavigation(nil)) + store.send(.fetchGalleries(keyword)) } .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.$route, case: /SearchReducer.Route.filters) { _ in + .sheet(unwrapping: $store.route, case: /SearchReducer.Route.filters) { _ in FiltersView(store: store.scope(state: \.filtersState, action: \.filters)) .accentColor(setting.accentColor).autoBlur(radius: blurRadius) } - .searchable(text: viewStore.$keyword) + .searchable(text: $store.keyword) .searchSuggestions { TagSuggestionView( - keyword: viewStore.$keyword, translations: tagTranslator.translations, + keyword: $store.keyword, translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion ) } .onSubmit(of: .search) { - viewStore.send(.fetchGalleries()) + store.send(.fetchGalleries()) } .onAppear { - if viewStore.galleries.isEmpty { + if store.galleries.isEmpty { DispatchQueue.main.async { - viewStore.send(.fetchGalleries(keyword)) + store.send(.fetchGalleries(keyword)) } } } .background(navigationLink) .toolbar(content: toolbar) - .navigationTitle(viewStore.lastKeyword) + .navigationTitle(store.lastKeyword) if DeviceUtil.isPad { content - .sheet(unwrapping: viewStore.$route, case: /SearchReducer.Route.detail) { route in + .sheet(unwrapping: $store.route, case: /SearchReducer.Route.detail) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -99,9 +97,9 @@ struct SearchView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.$route, case: /SearchReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: /SearchReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: \.detail), + store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -112,10 +110,10 @@ struct SearchView: View { CustomToolbarItem { ToolbarFeaturesMenu { FiltersButton { - viewStore.send(.setNavigation(.filters)) + store.send(.setNavigation(.filters)) } QuickSearchButton { - viewStore.send(.setNavigation(.quickSearch)) + store.send(.setNavigation(.quickSearch)) } } } diff --git a/EhPanda/View/Search/Support/QuickSearchReducer.swift b/EhPanda/View/Search/Support/QuickSearchReducer.swift index 59f02d04..56001f1f 100644 --- a/EhPanda/View/Search/Support/QuickSearchReducer.swift +++ b/EhPanda/View/Search/Support/QuickSearchReducer.swift @@ -26,11 +26,12 @@ struct QuickSearchReducer { case fetchQuickSearchWords } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var focusedField: FocusField? - @BindingState var editingWord: QuickSearchWord = .empty - @BindingState var listEditMode: EditMode = .inactive + var route: Route? + var focusedField: FocusField? + var editingWord: QuickSearchWord = .empty + var listEditMode: EditMode = .inactive var isListEditing: Bool { get { listEditMode == .active } set { listEditMode = newValue ? .active : .inactive } @@ -65,12 +66,12 @@ struct QuickSearchReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - case .binding: return .none diff --git a/EhPanda/View/Search/Support/QuickSearchView.swift b/EhPanda/View/Search/Support/QuickSearchView.swift index 605c548d..f39a503c 100644 --- a/EhPanda/View/Search/Support/QuickSearchView.swift +++ b/EhPanda/View/Search/Support/QuickSearchView.swift @@ -9,15 +9,13 @@ import SwiftUI import ComposableArchitecture struct QuickSearchView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let searchAction: (String) -> Void @FocusState private var focusedField: QuickSearchReducer.FocusField? init(store: StoreOf, searchAction: @escaping (String) -> Void) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.searchAction = searchAction } @@ -25,7 +23,7 @@ struct QuickSearchView: View { NavigationView { ZStack { List { - ForEach(viewStore.quickSearchWords) { word in + ForEach(store.quickSearchWords) { word in Button { searchAction(word.content) } label: { @@ -39,56 +37,56 @@ struct QuickSearchView: View { } .swipeActions(edge: .trailing) { Button { - viewStore.send(.setNavigation(.deleteWord(word))) + store.send(.setNavigation(.deleteWord(word))) } label: { Image(systemSymbol: .trash) } .tint(.red) Button { - viewStore.send(.setEditingWord(word)) - viewStore.send(.setNavigation(.editWord)) + store.send(.setEditingWord(word)) + store.send(.setNavigation(.editWord)) } label: { Image(systemSymbol: .squareAndPencil) } } - .withArrow(isVisible: !viewStore.isListEditing).padding(5) + .withArrow(isVisible: !store.isListEditing).padding(5) .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.delete, - unwrapping: viewStore.$route, + unwrapping: $store.route, case: /QuickSearchReducer.Route.deleteWord, matching: word ) { route in Button(L10n.Localizable.ConfirmationDialog.Button.delete, role: .destructive) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - viewStore.send(.deleteWord(route)) + store.send(.deleteWord(route)) } } } } .onDelete { offsets in - viewStore.send(.deleteWordWithOffsets(offsets)) + store.send(.deleteWordWithOffsets(offsets)) } .onMove { source, destination in - viewStore.send(.moveWord(source, destination)) + store.send(.moveWord(source, destination)) } } LoadingView().opacity( - viewStore.loadingState == .loading - && viewStore.quickSearchWords.isEmpty ? 1 : 0 + store.loadingState == .loading + && store.quickSearchWords.isEmpty ? 1 : 0 ) ErrorView(error: .notFound) .opacity( - viewStore.loadingState != .loading - && viewStore.quickSearchWords.isEmpty ? 1 : 0 + store.loadingState != .loading + && store.quickSearchWords.isEmpty ? 1 : 0 ) } - .synchronize(viewStore.$focusedField, $focusedField) - .environment(\.editMode, viewStore.$listEditMode) - .animation(.default, value: viewStore.quickSearchWords) - .animation(.default, value: viewStore.listEditMode) + .synchronize($store.focusedField, $focusedField) + .environment(\.editMode, $store.listEditMode) + .animation(.default, value: store.quickSearchWords) + .animation(.default, value: store.listEditMode) .onAppear { - if viewStore.quickSearchWords.isEmpty { - viewStore.send(.fetchQuickSearchWords) + if store.quickSearchWords.isEmpty { + store.send(.fetchQuickSearchWords) } } .toolbar(content: toolbar) @@ -109,41 +107,41 @@ struct QuickSearchView: View { private func toolbar() -> some ToolbarContent { CustomToolbarItem { Button { - viewStore.send(.setEditingWord(.empty)) - viewStore.send(.setNavigation(.newWord)) + store.send(.setEditingWord(.empty)) + store.send(.setNavigation(.newWord)) } label: { Image(systemSymbol: .plus) } Button { - viewStore.send(.toggleListEditing) + store.send(.toggleListEditing) } label: { Image(systemSymbol: .pencilCircle) - .symbolVariant(viewStore.isListEditing ? .fill : .none) + .symbolVariant(store.isListEditing ? .fill : .none) } } } @ViewBuilder private var navigationLinks: some View { - NavigationLink(unwrapping: viewStore.$route, case: /QuickSearchReducer.Route.newWord) { _ in + NavigationLink(unwrapping: $store.route, case: /QuickSearchReducer.Route.newWord) { _ in EditWordView( title: L10n.Localizable.QuickSearchView.Title.newWord, - word: viewStore.$editingWord, + word: $store.editingWord, focusedField: $focusedField, submitAction: onTextFieldSubmitted, confirmAction: { - viewStore.send(.appendWord) - viewStore.send(.setNavigation(nil)) + store.send(.appendWord) + store.send(.setNavigation(nil)) } ) } - NavigationLink(unwrapping: viewStore.$route, case: /QuickSearchReducer.Route.editWord) { _ in + NavigationLink(unwrapping: $store.route, case: /QuickSearchReducer.Route.editWord) { _ in EditWordView( title: L10n.Localizable.QuickSearchView.Title.editWord, - word: viewStore.$editingWord, + word: $store.editingWord, focusedField: $focusedField, submitAction: onTextFieldSubmitted, confirmAction: { - viewStore.send(.editWord) - viewStore.send(.setNavigation(nil)) + store.send(.editWord) + store.send(.setNavigation(nil)) } ) } diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift index faa8b8de..8910b8cc 100644 --- a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift @@ -20,10 +20,11 @@ struct AccountSettingReducer { case webView(URL) } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var ehCookiesState: CookiesState = .empty(.ehentai) - @BindingState var exCookiesState: CookiesState = .empty(.exhentai) + var route: Route? + var ehCookiesState: CookiesState = .empty(.ehentai) + var exCookiesState: CookiesState = .empty(.exhentai) var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded var loginState = LoginReducer.State() @@ -47,22 +48,18 @@ struct AccountSettingReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } + .onChange(of: \.ehCookiesState) { _, newValue in + Reduce({ _, _ in .run(operation: { _ in cookieClient.setCookies(state: newValue) }) }) + } + .onChange(of: \.exCookiesState) { _, newValue in + Reduce({ _, _ in .run(operation: { _ in cookieClient.setCookies(state: newValue) }) }) + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - - case .binding(\.$ehCookiesState): - return .run { [state] _ in - cookieClient.setCookies(state: state.ehCookiesState) - } - - case .binding(\.$exCookiesState): - return .run { [state] _ in - cookieClient.setCookies(state: state.exCookiesState) - } - case .binding: return .none diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift index 5b911b38..99bd5aa0 100644 --- a/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift @@ -9,8 +9,7 @@ import SwiftUI import ComposableArchitecture struct AccountSettingView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf @Binding private var galleryHost: GalleryHost @Binding private var showsNewDawnGreeting: Bool private let bypassesSNIFiltering: Bool @@ -22,7 +21,6 @@ struct AccountSettingView: View { bypassesSNIFiltering: Bool, blurRadius: Double ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) _galleryHost = galleryHost _showsNewDawnGreeting = showsNewDawnGreeting self.bypassesSNIFiltering = bypassesSNIFiltering @@ -40,36 +38,36 @@ struct AccountSettingView: View { } .pickerStyle(.segmented) AccountSection( - route: viewStore.$route, + route: $store.route, showsNewDawnGreeting: $showsNewDawnGreeting, bypassesSNIFiltering: bypassesSNIFiltering, - loginAction: { viewStore.send(.setNavigation(.login)) }, + loginAction: { store.send(.setNavigation(.login)) }, logoutAction: { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - viewStore.send(.onLogoutConfirmButtonTapped) + store.send(.onLogoutConfirmButtonTapped) } }, - logoutDialogAction: { viewStore.send(.setNavigation(.logout)) }, - configureAccountAction: { viewStore.send(.setNavigation(.ehSetting)) }, - manageTagsAction: { viewStore.send(.setNavigation(.webView(Defaults.URL.myTags))) } + logoutDialogAction: { store.send(.setNavigation(.logout)) }, + configureAccountAction: { store.send(.setNavigation(.ehSetting)) }, + manageTagsAction: { store.send(.setNavigation(.webView(Defaults.URL.myTags))) } ) } CookieSection( - ehCookiesState: viewStore.$ehCookiesState, - exCookiesState: viewStore.$exCookiesState, - copyAction: { viewStore.send(.copyCookies($0)) } + ehCookiesState: $store.ehCookiesState, + exCookiesState: $store.exCookiesState, + copyAction: { store.send(.copyCookies($0)) } ) } .progressHUD( - config: viewStore.hudConfig, - unwrapping: viewStore.$route, + config: store.hudConfig, + unwrapping: $store.route, case: /AccountSettingReducer.Route.hud ) - .sheet(unwrapping: viewStore.$route, case: /AccountSettingReducer.Route.webView) { route in + .sheet(unwrapping: $store.route, case: /AccountSettingReducer.Route.webView) { route in WebView(url: route.wrappedValue) .autoBlur(radius: blurRadius) } - .onAppear { viewStore.send(.loadCookies) } + .onAppear { store.send(.loadCookies) } .background(navigationLinks) .navigationTitle(L10n.Localizable.AccountSettingView.Title.account) } @@ -78,13 +76,13 @@ struct AccountSettingView: View { // MARK: NavigationLinks private extension AccountSettingView { @ViewBuilder var navigationLinks: some View { - NavigationLink(unwrapping: viewStore.$route, case: /AccountSettingReducer.Route.login) { _ in + NavigationLink(unwrapping: $store.route, case: /AccountSettingReducer.Route.login) { _ in LoginView( store: store.scope(state: \.loginState, action: \.login), bypassesSNIFiltering: bypassesSNIFiltering, blurRadius: blurRadius ) } - NavigationLink(unwrapping: viewStore.$route, case: /AccountSettingReducer.Route.ehSetting) { _ in + NavigationLink(unwrapping: $store.route, case: /AccountSettingReducer.Route.ehSetting) { _ in EhSettingView( store: store.scope(state: \.ehSettingState, action: \.ehSetting), bypassesSNIFiltering: bypassesSNIFiltering, blurRadius: blurRadius diff --git a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift index c7e57396..1b45dbe4 100644 --- a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift +++ b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift @@ -13,8 +13,9 @@ struct AppearanceSettingReducer { case appIcon } + @ObservableState struct State: Equatable { - @BindingState var route: Route? + var route: Route? } enum Action: BindableAction, Equatable { diff --git a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift index 4d1ee0cc..b3c4f084 100644 --- a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift +++ b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift @@ -9,8 +9,7 @@ import SwiftUI import ComposableArchitecture struct AppearanceSettingView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf @Binding private var preferredColorScheme: PreferredColorScheme @Binding private var accentColor: Color @@ -31,7 +30,6 @@ struct AppearanceSettingView: View { displaysJapaneseTitle: Binding ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) _preferredColorScheme = preferredColorScheme _accentColor = accentColor _appIconType = appIconType @@ -58,7 +56,7 @@ struct AppearanceSettingView: View { ColorPicker(L10n.Localizable.AppearanceSettingView.Title.tintColor, selection: $accentColor) Button(L10n.Localizable.AppearanceSettingView.Button.appIcon) { - viewStore.send(.setNavigation(.appIcon)) + store.send(.setNavigation(.appIcon)) } .foregroundStyle(.primary) .withArrow() @@ -107,7 +105,7 @@ struct AppearanceSettingView: View { } private var navigationLink: some View { - NavigationLink(unwrapping: viewStore.$route, case: /AppearanceSettingReducer.Route.appIcon) { _ in + NavigationLink(unwrapping: $store.route, case: /AppearanceSettingReducer.Route.appIcon) { _ in AppIconView(appIconType: $appIconType) } } diff --git a/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift b/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift index cd408e3e..26471655 100644 --- a/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift +++ b/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift @@ -20,11 +20,12 @@ struct EhSettingReducer { case fetchEhSetting, submitChanges, performAction } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var editingProfileName = "" - @BindingState var ehSetting: EhSetting? - @BindingState var ehProfile: EhProfile? + var route: Route? + var editingProfileName = "" + var ehSetting: EhSetting? + var ehProfile: EhProfile? var loadingState: LoadingState = .idle var submittingState: LoadingState = .idle diff --git a/EhPanda/View/Setting/EhSetting/EhSettingView.swift b/EhPanda/View/Setting/EhSetting/EhSettingView.swift index 7ee6bbdf..ae9d2fd2 100644 --- a/EhPanda/View/Setting/EhSetting/EhSettingView.swift +++ b/EhPanda/View/Setting/EhSetting/EhSettingView.swift @@ -9,14 +9,12 @@ import SwiftUI import ComposableArchitecture struct EhSettingView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let bypassesSNIFiltering: Bool private let blurRadius: Double init(store: StoreOf, bypassesSNIFiltering: Bool, blurRadius: Double) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.bypassesSNIFiltering = bypassesSNIFiltering self.blurRadius = blurRadius } @@ -25,32 +23,32 @@ struct EhSettingView: View { var body: some View { ZStack { // workaround: Stay if-else approach - if viewStore.loadingState == .loading || viewStore.submittingState == .loading { + if store.loadingState == .loading || store.submittingState == .loading { LoadingView() .tint(nil) - } else if case .failed(let error) = viewStore.loadingState { - ErrorView(error: error, action: { viewStore.send(.fetchEhSetting) }) + } else if case .failed(let error) = store.loadingState { + ErrorView(error: error, action: { store.send(.fetchEhSetting) }) .tint(nil) } // Using `Binding.init` will crash the app - else if let ehSetting = Binding(unwrapping: viewStore.$ehSetting), - let ehProfile = Binding(unwrapping: viewStore.$ehProfile) + else if let ehSetting = Binding(unwrapping: $store.ehSetting), + let ehProfile = Binding(unwrapping: $store.ehProfile) { form(ehSetting: ehSetting, ehProfile: ehProfile) .transition(.opacity.animation(.default)) } } .onAppear { - if viewStore.ehSetting == nil { - viewStore.send(.fetchEhSetting) + if store.ehSetting == nil { + store.send(.fetchEhSetting) } } .onDisappear { - if let profileSet = viewStore.ehSetting?.ehpandaProfile?.value { - viewStore.send(.setDefaultProfile(profileSet)) + if let profileSet = store.ehSetting?.ehpandaProfile?.value { + store.send(.setDefaultProfile(profileSet)) } } - .sheet(unwrapping: viewStore.$route, case: /EhSettingReducer.Route.webView) { route in + .sheet(unwrapping: $store.route, case: /EhSettingReducer.Route.webView) { route in WebView(url: route.wrappedValue) .autoBlur(radius: blurRadius) } @@ -62,19 +60,19 @@ struct EhSettingView: View { Form { Group { EhProfileSection( - route: viewStore.$route, + route: $store.route, ehSetting: ehSetting, ehProfile: ehProfile, - editingProfileName: viewStore.$editingProfileName, + editingProfileName: $store.editingProfileName, deleteAction: { - if let value = viewStore.ehProfile?.value { + if let value = store.ehProfile?.value { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - viewStore.send(.performAction(.delete, nil, value)) + store.send(.performAction(.delete, nil, value)) } } }, - deleteDialogAction: { viewStore.send(.setNavigation(.deleteProfile)) }, - performEhProfileAction: { viewStore.send(.performAction($0, $1, $2)) } + deleteDialogAction: { store.send(.setNavigation(.deleteProfile)) }, + performEhProfileAction: { store.send(.performAction($0, $1, $2)) } ) ImageLoadSettingsSection(ehSetting: ehSetting) @@ -110,7 +108,7 @@ struct EhSettingView: View { Group { ToolbarItem(placement: .navigationBarTrailing) { Button { - viewStore.send(.setNavigation(.webView(Defaults.URL.uConfig))) + store.send(.setNavigation(.webView(Defaults.URL.uConfig))) } label: { Image(systemSymbol: .globe) } @@ -119,16 +117,16 @@ struct EhSettingView: View { ToolbarItem(placement: .confirmationAction) { Button { - viewStore.send(.submitChanges) + store.send(.submitChanges) } label: { Image(systemSymbol: .icloudAndArrowUp) } - .disabled(viewStore.ehSetting == nil) + .disabled(store.ehSetting == nil) } ToolbarItem(placement: .keyboard) { Button(L10n.Localizable.EhSettingView.ToolbarItem.Button.done) { - viewStore.send(.setKeyboardHidden) + store.send(.setKeyboardHidden) } .frame(maxWidth: .infinity, alignment: .trailing) } diff --git a/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift b/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift index ddf1980c..a2151e0c 100644 --- a/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift +++ b/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift @@ -17,8 +17,9 @@ struct GeneralSettingReducer { case removeCustomTranslations } + @ObservableState struct State: Equatable { - @BindingState var route: Route? + var route: Route? var loadingState: LoadingState = .idle var diskImageCacheSize = "0 KB" @@ -50,12 +51,12 @@ struct GeneralSettingReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } Reduce { state, action in switch action { - case .binding(\.$route): - return state.route == nil ? .send(.clearSubStates) : .none - case .binding: return .none diff --git a/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift b/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift index 2a9ab938..a1961c14 100644 --- a/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift +++ b/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift @@ -10,8 +10,7 @@ import FilePicker import ComposableArchitecture struct GeneralSettingView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let tagTranslatorLoadingState: LoadingState private let tagTranslatorEmpty: Bool private let tagTranslatorHasCustomTranslations: Bool @@ -34,7 +33,6 @@ struct GeneralSettingView: View { autoLockPolicy: Binding ) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.tagTranslatorLoadingState = tagTranslatorLoadingState self.tagTranslatorEmpty = tagTranslatorEmpty self.tagTranslatorHasCustomTranslations = tagTranslatorHasCustomTranslations @@ -60,12 +58,12 @@ struct GeneralSettingView: View { Text(L10n.Localizable.GeneralSettingView.Title.language) Spacer() Button(language) { - viewStore.send(.navigateToSystemSetting) + store.send(.navigateToSystemSetting) } .foregroundStyle(.tint) } Button(L10n.Localizable.GeneralSettingView.Button.logs) { - viewStore.send(.setNavigation(.logs)) + store.send(.setNavigation(.logs)) } .foregroundColor(.primary).withArrow() } @@ -96,22 +94,22 @@ struct GeneralSettingView: View { title: L10n.Localizable.GeneralSettingView.Button.importCustomTranslations ) { urls in if let url = urls.first { - viewStore.send(.onTranslationsFilePicked(url)) + store.send(.onTranslationsFilePicked(url)) } } if tagTranslatorHasCustomTranslations { Button( L10n.Localizable.GeneralSettingView.Button.removeCustomTranslations, - role: .destructive, action: { viewStore.send(.setNavigation(.removeCustomTranslations)) } + role: .destructive, action: { store.send(.setNavigation(.removeCustomTranslations)) } ) .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.removeCustomTranslations, - unwrapping: viewStore.$route, + unwrapping: $store.route, case: /GeneralSettingReducer.Route.removeCustomTranslations ) { Button(L10n.Localizable.ConfirmationDialog.Button.remove, role: .destructive) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - viewStore.send(.onRemoveCustomTranslations) + store.send(.onRemoveCustomTranslations) } } } @@ -138,7 +136,7 @@ struct GeneralSettingView: View { } } .pickerStyle(.menu) - if viewStore.passcodeNotSet && autoLockPolicy != .never { + if store.passcodeNotSet && autoLockPolicy != .never { Image(systemSymbol: .exclamationmarkTriangleFill).foregroundStyle(.yellow) } } @@ -153,22 +151,22 @@ struct GeneralSettingView: View { } Section(L10n.Localizable.GeneralSettingView.Section.Title.caches) { Button { - viewStore.send(.setNavigation(.clearCache)) + store.send(.setNavigation(.clearCache)) } label: { HStack { Text(L10n.Localizable.GeneralSettingView.Button.clearImageCaches) Spacer() - Text(viewStore.diskImageCacheSize).foregroundStyle(.tint) + Text(store.diskImageCacheSize).foregroundStyle(.tint) } .foregroundColor(.primary) } .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.clear, - unwrapping: viewStore.$route, + unwrapping: $store.route, case: /GeneralSettingReducer.Route.clearCache ) { Button(L10n.Localizable.ConfirmationDialog.Button.clear, role: .destructive) { - viewStore.send(.clearWebImageCache) + store.send(.clearWebImageCache) } } } @@ -178,15 +176,15 @@ struct GeneralSettingView: View { .animation(.default, value: enablesTagsExtension) .animation(.default, value: tagTranslatorEmpty) .onAppear { - viewStore.send(.checkPasscodeSetting) - viewStore.send(.calculateWebImageDiskCache) + store.send(.checkPasscodeSetting) + store.send(.calculateWebImageDiskCache) } .background(navigationLink) .navigationTitle(L10n.Localizable.GeneralSettingView.Title.general) } private var navigationLink: some View { - NavigationLink(unwrapping: viewStore.$route, case: /GeneralSettingReducer.Route.logs) { _ in + NavigationLink(unwrapping: $store.route, case: /GeneralSettingReducer.Route.logs) { _ in LogsView(store: store.scope(state: \.logsState, action: \.logs)) } } diff --git a/EhPanda/View/Setting/Login/LoginReducer.swift b/EhPanda/View/Setting/Login/LoginReducer.swift index 7342a31c..e289703e 100644 --- a/EhPanda/View/Setting/Login/LoginReducer.swift +++ b/EhPanda/View/Setting/Login/LoginReducer.swift @@ -23,11 +23,12 @@ struct LoginReducer { case password } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var focusedField: FocusedField? - @BindingState var username = "" - @BindingState var password = "" + var route: Route? + var focusedField: FocusedField? + var username = "" + var password = "" var loginState: LoadingState = .idle var loginButtonDisabled: Bool { diff --git a/EhPanda/View/Setting/Login/LoginView.swift b/EhPanda/View/Setting/Login/LoginView.swift index 9a300d58..e9ee2188 100644 --- a/EhPanda/View/Setting/Login/LoginView.swift +++ b/EhPanda/View/Setting/Login/LoginView.swift @@ -9,8 +9,7 @@ import SwiftUI import ComposableArchitecture struct LoginView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let bypassesSNIFiltering: Bool private let blurRadius: Double @@ -18,7 +17,6 @@ struct LoginView: View { init(store: StoreOf, bypassesSNIFiltering: Bool, blurRadius: Double) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.bypassesSNIFiltering = bypassesSNIFiltering self.blurRadius = blurRadius } @@ -35,30 +33,30 @@ struct LoginView: View { VStack(spacing: 15) { Group { LoginTextField( - focusedField: $focusedField, text: viewStore.$username, + focusedField: $focusedField, text: $store.username, description: L10n.Localizable.LoginView.Title.username, isPassword: false ) LoginTextField( - focusedField: $focusedField, text: viewStore.$password, + focusedField: $focusedField, text: $store.password, description: L10n.Localizable.LoginView.Title.password, isPassword: true ) } .padding(.horizontal, proxy.size.width * 0.2) Button { - viewStore.send(.login) + store.send(.login) } label: { Image(systemSymbol: .chevronForwardCircleFill) } - .overlay { ProgressView().tint(nil).opacity(viewStore.loginState == .loading ? 1 : 0) } - .imageScale(.large).font(.largeTitle).foregroundColor(viewStore.loginButtonColor) - .disabled(viewStore.loginButtonDisabled).padding(.top, 30) + .overlay { ProgressView().tint(nil).opacity(store.loginState == .loading ? 1 : 0) } + .imageScale(.large).font(.largeTitle).foregroundColor(store.loginButtonColor) + .disabled(store.loginButtonDisabled).padding(.top, 30) } } } - .synchronize(viewStore.$focusedField, $focusedField) - .sheet(unwrapping: viewStore.$route, case: /LoginReducer.Route.webView) { route in + .synchronize($store.focusedField, $focusedField) + .sheet(unwrapping: $store.route, case: /LoginReducer.Route.webView) { route in WebView(url: route.wrappedValue) { - viewStore.send(.loginDone(.success(nil))) + store.send(.loginDone(.success(nil))) } .autoBlur(radius: blurRadius) } @@ -68,10 +66,10 @@ struct LoginView: View { focusedField = .password default: focusedField = nil - viewStore.send(.login) + store.send(.login) } } - .animation(.default, value: viewStore.loginState) + .animation(.default, value: store.loginState) .toolbar(content: toolbar) .navigationTitle(L10n.Localizable.LoginView.Title.login) .ignoresSafeArea() @@ -80,7 +78,7 @@ struct LoginView: View { private func toolbar() -> some ToolbarContent { ToolbarItem(placement: .navigationBarTrailing) { Button { - viewStore.send(.setNavigation(.webView(Defaults.URL.webLogin))) + store.send(.setNavigation(.webView(Defaults.URL.webLogin))) } label: { Image(systemSymbol: .globe) } diff --git a/EhPanda/View/Setting/Logs/LogsReducer.swift b/EhPanda/View/Setting/Logs/LogsReducer.swift index ea708a4d..906ec4d1 100644 --- a/EhPanda/View/Setting/Logs/LogsReducer.swift +++ b/EhPanda/View/Setting/Logs/LogsReducer.swift @@ -18,8 +18,9 @@ struct LogsReducer { case fetchLogs } + @ObservableState struct State: Equatable { - @BindingState var route: Route? + var route: Route? var loadingState: LoadingState = .idle var logs = [Log]() } diff --git a/EhPanda/View/Setting/Logs/LogsView.swift b/EhPanda/View/Setting/Logs/LogsView.swift index 7ab24546..16b6550d 100644 --- a/EhPanda/View/Setting/Logs/LogsView.swift +++ b/EhPanda/View/Setting/Logs/LogsView.swift @@ -9,25 +9,23 @@ import SwiftUI import ComposableArchitecture struct LogsView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf init(store: StoreOf) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) } var body: some View { ZStack { - List(viewStore.logs) { log in + List(store.logs) { log in Button { - viewStore.send(.setNavigation(.log(log))) + store.send(.setNavigation(.log(log))) } label: { - LogCell(log: log, isLatest: log == viewStore.logs.first) + LogCell(log: log, isLatest: log == store.logs.first) } .swipeActions { Button { - viewStore.send(.deleteLog(log.fileName)) + store.send(.deleteLog(log.fileName)) } label: { Image(systemSymbol: .trash) } @@ -35,18 +33,18 @@ struct LogsView: View { } .foregroundColor(.primary) } - .opacity(viewStore.logs.isEmpty ? 0 : 1) - LoadingView().opacity(viewStore.loadingState == .loading && viewStore.logs.isEmpty ? 1 : 0) - let error = (/LoadingState.failed).extract(from: viewStore.loadingState) + .opacity(store.logs.isEmpty ? 0 : 1) + LoadingView().opacity(store.loadingState == .loading && store.logs.isEmpty ? 1 : 0) + let error = (/LoadingState.failed).extract(from: store.loadingState) ErrorView(error: error ?? .notFound) { - viewStore.send(.fetchLogs) + store.send(.fetchLogs) } - .opacity(error != nil && viewStore.logs.isEmpty ? 1 : 0) + .opacity(error != nil && store.logs.isEmpty ? 1 : 0) } .onAppear { - if viewStore.logs.isEmpty { + if store.logs.isEmpty { DispatchQueue.main.async { - viewStore.send(.fetchLogs) + store.send(.fetchLogs) } } } @@ -56,14 +54,14 @@ struct LogsView: View { } private var navigationLink: some View { - NavigationLink(unwrapping: viewStore.$route, case: /LogsReducer.Route.log) { route in + NavigationLink(unwrapping: $store.route, case: /LogsReducer.Route.log) { route in LogView(log: route.wrappedValue) } } private func toolbar() -> some ToolbarContent { ToolbarItem(placement: .navigationBarTrailing) { Button { - viewStore.send(.navigateToFileApp) + store.send(.navigateToFileApp) } label: { Image(systemSymbol: .folderBadgeGearshape) } diff --git a/EhPanda/View/Setting/SettingReducer.swift b/EhPanda/View/Setting/SettingReducer.swift index 7c206fcf..9ed13e49 100644 --- a/EhPanda/View/Setting/SettingReducer.swift +++ b/EhPanda/View/Setting/SettingReducer.swift @@ -22,15 +22,16 @@ struct SettingReducer { case about } + @ObservableState struct State: Equatable { // AppEnvStorage - @BindingState var setting = Setting() + var setting = Setting() var tagTranslator = TagTranslator() var user = User() var hasLoadedInitialSetting = false - @BindingState var route: Route? + var route: Route? var tagTranslatorLoadingState: LoadingState = .idle var accountSettingState = AccountSettingReducer.State() @@ -113,6 +114,9 @@ struct SettingReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.setting) { _, _ in + Reduce({ _, _ in .send(.syncSetting) }) + } .onChange(of: \.setting.galleryHost) { _, newValue in Reduce { _, _ in .merge( @@ -206,12 +210,6 @@ struct SettingReducer { Reduce { state, action in switch action { - case .binding(\.$setting): - return .send(.syncSetting) - - case .binding(\.$route): - return .none - case .binding: return .merge( .send(.syncUser), diff --git a/EhPanda/View/Setting/SettingView.swift b/EhPanda/View/Setting/SettingView.swift index 0f106478..f8b638e7 100644 --- a/EhPanda/View/Setting/SettingView.swift +++ b/EhPanda/View/Setting/SettingView.swift @@ -10,13 +10,11 @@ import SFSafeSymbols import ComposableArchitecture struct SettingView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf private let blurRadius: Double init(store: StoreOf, blurRadius: Double) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) self.blurRadius = blurRadius } @@ -27,7 +25,7 @@ struct SettingView: View { VStack(spacing: 0) { ForEach(SettingReducer.Route.allCases) { route in SettingRow(rowType: route) { - viewStore.send(.setNavigation($0)) + store.send(.setNavigation($0)) } } } @@ -42,65 +40,65 @@ struct SettingView: View { // MARK: NavigationLinks private extension SettingView { @ViewBuilder var navigationLinks: some View { - NavigationLink(unwrapping: viewStore.$route, case: /SettingReducer.Route.account) { _ in + NavigationLink(unwrapping: $store.route, case: /SettingReducer.Route.account) { _ in AccountSettingView( store: store.scope(state: \.accountSettingState, action: \.account), - galleryHost: viewStore.$setting.galleryHost, - showsNewDawnGreeting: viewStore.$setting.showsNewDawnGreeting, - bypassesSNIFiltering: viewStore.setting.bypassesSNIFiltering, + galleryHost: $store.setting.galleryHost, + showsNewDawnGreeting: $store.setting.showsNewDawnGreeting, + bypassesSNIFiltering: store.setting.bypassesSNIFiltering, blurRadius: blurRadius ) - .tint(viewStore.setting.accentColor) + .tint(store.setting.accentColor) } - NavigationLink(unwrapping: viewStore.$route, case: /SettingReducer.Route.general) { _ in + NavigationLink(unwrapping: $store.route, case: /SettingReducer.Route.general) { _ in GeneralSettingView( store: store.scope(state: \.generalSettingState, action: \.general), - tagTranslatorLoadingState: viewStore.tagTranslatorLoadingState, - tagTranslatorEmpty: viewStore.tagTranslator.translations.isEmpty, - tagTranslatorHasCustomTranslations: viewStore.tagTranslator.hasCustomTranslations, - enablesTagsExtension: viewStore.$setting.enablesTagsExtension, - translatesTags: viewStore.$setting.translatesTags, - showsTagsSearchSuggestion: viewStore.$setting.showsTagsSearchSuggestion, - showsImagesInTags: viewStore.$setting.showsImagesInTags, - redirectsLinksToSelectedHost: viewStore.$setting.redirectsLinksToSelectedHost, - detectsLinksFromClipboard: viewStore.$setting.detectsLinksFromClipboard, - backgroundBlurRadius: viewStore.$setting.backgroundBlurRadius, - autoLockPolicy: viewStore.$setting.autoLockPolicy + tagTranslatorLoadingState: store.tagTranslatorLoadingState, + tagTranslatorEmpty: store.tagTranslator.translations.isEmpty, + tagTranslatorHasCustomTranslations: store.tagTranslator.hasCustomTranslations, + enablesTagsExtension: $store.setting.enablesTagsExtension, + translatesTags: $store.setting.translatesTags, + showsTagsSearchSuggestion: $store.setting.showsTagsSearchSuggestion, + showsImagesInTags: $store.setting.showsImagesInTags, + redirectsLinksToSelectedHost: $store.setting.redirectsLinksToSelectedHost, + detectsLinksFromClipboard: $store.setting.detectsLinksFromClipboard, + backgroundBlurRadius: $store.setting.backgroundBlurRadius, + autoLockPolicy: $store.setting.autoLockPolicy ) - .tint(viewStore.setting.accentColor) + .tint(store.setting.accentColor) } - NavigationLink(unwrapping: viewStore.$route, case: /SettingReducer.Route.appearance) { _ in + NavigationLink(unwrapping: $store.route, case: /SettingReducer.Route.appearance) { _ in AppearanceSettingView( store: store.scope(state: \.appearanceSettingState, action: \.appearance), - preferredColorScheme: viewStore.$setting.preferredColorScheme, - accentColor: viewStore.$setting.accentColor, - appIconType: viewStore.$setting.appIconType, - listDisplayMode: viewStore.$setting.listDisplayMode, - showsTagsInList: viewStore.$setting.showsTagsInList, - listTagsNumberMaximum: viewStore.$setting.listTagsNumberMaximum, - displaysJapaneseTitle: viewStore.$setting.displaysJapaneseTitle + preferredColorScheme: $store.setting.preferredColorScheme, + accentColor: $store.setting.accentColor, + appIconType: $store.setting.appIconType, + listDisplayMode: $store.setting.listDisplayMode, + showsTagsInList: $store.setting.showsTagsInList, + listTagsNumberMaximum: $store.setting.listTagsNumberMaximum, + displaysJapaneseTitle: $store.setting.displaysJapaneseTitle ) - .tint(viewStore.setting.accentColor) + .tint(store.setting.accentColor) } - NavigationLink(unwrapping: viewStore.$route, case: /SettingReducer.Route.reading) { _ in + NavigationLink(unwrapping: $store.route, case: /SettingReducer.Route.reading) { _ in ReadingSettingView( - readingDirection: viewStore.$setting.readingDirection, - prefetchLimit: viewStore.$setting.prefetchLimit, - enablesLandscape: viewStore.$setting.enablesLandscape, - contentDividerHeight: viewStore.$setting.contentDividerHeight, - maximumScaleFactor: viewStore.$setting.maximumScaleFactor, - doubleTapScaleFactor: viewStore.$setting.doubleTapScaleFactor + readingDirection: $store.setting.readingDirection, + prefetchLimit: $store.setting.prefetchLimit, + enablesLandscape: $store.setting.enablesLandscape, + contentDividerHeight: $store.setting.contentDividerHeight, + maximumScaleFactor: $store.setting.maximumScaleFactor, + doubleTapScaleFactor: $store.setting.doubleTapScaleFactor ) - .tint(viewStore.setting.accentColor) + .tint(store.setting.accentColor) } - NavigationLink(unwrapping: viewStore.$route, case: /SettingReducer.Route.laboratory) { _ in + NavigationLink(unwrapping: $store.route, case: /SettingReducer.Route.laboratory) { _ in LaboratorySettingView( - bypassesSNIFiltering: viewStore.$setting.bypassesSNIFiltering + bypassesSNIFiltering: $store.setting.bypassesSNIFiltering ) - .tint(viewStore.setting.accentColor) + .tint(store.setting.accentColor) } - NavigationLink(unwrapping: viewStore.$route, case: /SettingReducer.Route.about) { _ in - AboutView().tint(viewStore.setting.accentColor) + NavigationLink(unwrapping: $store.route, case: /SettingReducer.Route.about) { _ in + AboutView().tint(store.setting.accentColor) } } } diff --git a/EhPanda/View/Support/FiltersReducer.swift b/EhPanda/View/Support/FiltersReducer.swift index 9851b9a4..112ea0a4 100644 --- a/EhPanda/View/Support/FiltersReducer.swift +++ b/EhPanda/View/Support/FiltersReducer.swift @@ -18,14 +18,15 @@ struct FiltersReducer { case upper } + @ObservableState struct State: Equatable { - @BindingState var route: Route? - @BindingState var filterRange: FilterRange = .search - @BindingState var focusedBound: FocusedBound? + var route: Route? + var filterRange: FilterRange = .search + var focusedBound: FocusedBound? - @BindingState var searchFilter = Filter() - @BindingState var globalFilter = Filter() - @BindingState var watchedFilter = Filter() + var searchFilter = Filter() + var globalFilter = Filter() + var watchedFilter = Filter() } enum Action: BindableAction, Equatable { @@ -43,21 +44,27 @@ struct FiltersReducer { var body: some Reducer { BindingReducer() + .onChange(of: \.searchFilter) { _, _ in + Reduce { state, _ in + state.searchFilter.fixInvalidData() + return .send(.syncFilter(.search)) + } + } + .onChange(of: \.globalFilter) { _, _ in + Reduce { state, _ in + state.globalFilter.fixInvalidData() + return .send(.syncFilter(.global)) + } + } + .onChange(of: \.watchedFilter) { _, _ in + Reduce { state, _ in + state.watchedFilter.fixInvalidData() + return .send(.syncFilter(.watched)) + } + } Reduce { state, action in switch action { - case .binding(\.$searchFilter): - state.searchFilter.fixInvalidData() - return .send(.syncFilter(.search)) - - case .binding(\.$globalFilter): - state.globalFilter.fixInvalidData() - return .send(.syncFilter(.global)) - - case .binding(\.$watchedFilter): - state.watchedFilter.fixInvalidData() - return .send(.syncFilter(.watched)) - case .binding: return .none diff --git a/EhPanda/View/Support/FiltersView.swift b/EhPanda/View/Support/FiltersView.swift index c7dc475a..483998bb 100644 --- a/EhPanda/View/Support/FiltersView.swift +++ b/EhPanda/View/Support/FiltersView.swift @@ -9,24 +9,22 @@ import SwiftUI import ComposableArchitecture struct FiltersView: View { - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf @FocusState private var focusedBound: FiltersReducer.FocusedBound? init(store: StoreOf) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) } private var filter: Binding { - switch viewStore.filterRange { + switch store.filterRange { case .search: - return viewStore.$searchFilter + return $store.searchFilter case .global: - return viewStore.$globalFilter + return $store.globalFilter case .watched: - return viewStore.$watchedFilter + return $store.watchedFilter } } @@ -35,19 +33,19 @@ struct FiltersView: View { NavigationView { Form { BasicSection( - route: viewStore.$route, - filter: filter, filterRange: viewStore.$filterRange, - resetFiltersAction: { viewStore.send(.resetFilters) }, - resetFiltersDialogAction: { viewStore.send(.setNavigation(.resetFilters)) } + route: $store.route, + filter: filter, filterRange: $store.filterRange, + resetFiltersAction: { store.send(.resetFilters) }, + resetFiltersDialogAction: { store.send(.setNavigation(.resetFilters)) } ) AdvancedSection( filter: filter, focusedBound: $focusedBound, - submitAction: { viewStore.send(.onTextFieldSubmitted) } + submitAction: { store.send(.onTextFieldSubmitted) } ) } - .synchronize(viewStore.$focusedBound, $focusedBound) + .synchronize($store.focusedBound, $focusedBound) .navigationTitle(L10n.Localizable.FiltersView.Title.filters) - .onAppear { viewStore.send(.fetchFilters) } + .onAppear { store.send(.fetchFilters) } } } } diff --git a/EhPanda/View/TabBar/TabBarReducer.swift b/EhPanda/View/TabBar/TabBarReducer.swift index d5911530..d550073b 100644 --- a/EhPanda/View/TabBar/TabBarReducer.swift +++ b/EhPanda/View/TabBar/TabBarReducer.swift @@ -9,6 +9,7 @@ import ComposableArchitecture @Reducer struct TabBarReducer { + @ObservableState struct State: Equatable { var tabBarItemType: TabBarItemType = .home } diff --git a/EhPanda/View/TabBar/TabBarView.swift b/EhPanda/View/TabBar/TabBarView.swift index 5300492c..ebd0ad53 100644 --- a/EhPanda/View/TabBar/TabBarView.swift +++ b/EhPanda/View/TabBar/TabBarView.swift @@ -11,20 +11,18 @@ import ComposableArchitecture struct TabBarView: View { @Environment(\.scenePhase) private var scenePhase - private let store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Bindable private var store: StoreOf init(store: StoreOf) { self.store = store - viewStore = ViewStore(store, observe: { $0 }) } var body: some View { ZStack { TabView( selection: .init( - get: { viewStore.tabBarState.tabBarItemType }, - set: { viewStore.send(.tabBar(.setTabBarItemType($0))) } + get: { store.tabBarState.tabBarItemType }, + set: { store.send(.tabBar(.setTabBarItemType($0))) } ) ) { ForEach(TabBarItemType.allCases) { type in @@ -33,83 +31,83 @@ struct TabBarView: View { case .home: HomeView( store: store.scope(state: \.homeState, action: \.home), - user: viewStore.settingState.user, - setting: viewStore.$settingState.setting, - blurRadius: viewStore.appLockState.blurRadius, - tagTranslator: viewStore.settingState.tagTranslator + user: store.settingState.user, + setting: $store.settingState.setting, + blurRadius: store.appLockState.blurRadius, + tagTranslator: store.settingState.tagTranslator ) case .favorites: FavoritesView( store: store.scope(state: \.favoritesState, action: \.favorites), - user: viewStore.settingState.user, - setting: viewStore.$settingState.setting, - blurRadius: viewStore.appLockState.blurRadius, - tagTranslator: viewStore.settingState.tagTranslator + user: store.settingState.user, + setting: $store.settingState.setting, + blurRadius: store.appLockState.blurRadius, + tagTranslator: store.settingState.tagTranslator ) case .search: SearchRootView( store: store.scope(state: \.searchRootState, action: \.searchRoot), - user: viewStore.settingState.user, - setting: viewStore.$settingState.setting, - blurRadius: viewStore.appLockState.blurRadius, - tagTranslator: viewStore.settingState.tagTranslator + user: store.settingState.user, + setting: $store.settingState.setting, + blurRadius: store.appLockState.blurRadius, + tagTranslator: store.settingState.tagTranslator ) case .setting: SettingView( store: store.scope(state: \.settingState, action: \.setting), - blurRadius: viewStore.appLockState.blurRadius + blurRadius: store.appLockState.blurRadius ) } } .tabItem(type.label).tag(type) } - .accentColor(viewStore.settingState.setting.accentColor) + .accentColor(store.settingState.setting.accentColor) } - .autoBlur(radius: viewStore.appLockState.blurRadius) + .autoBlur(radius: store.appLockState.blurRadius) Button { - viewStore.send(.appLock(.authorize)) + store.send(.appLock(.authorize)) } label: { Image(systemSymbol: .lockFill) } - .font(.system(size: 80)).opacity(viewStore.appLockState.isAppLocked ? 1 : 0) + .font(.system(size: 80)).opacity(store.appLockState.isAppLocked ? 1 : 0) } - .sheet(unwrapping: viewStore.$appRouteState.route, case: /AppRouteReducer.Route.newDawn) { route in + .sheet(unwrapping: $store.appRouteState.route, case: /AppRouteReducer.Route.newDawn) { route in NewDawnView(greeting: route.wrappedValue) - .autoBlur(radius: viewStore.appLockState.blurRadius) + .autoBlur(radius: store.appLockState.blurRadius) } - .sheet(unwrapping: viewStore.$appRouteState.route, case: /AppRouteReducer.Route.setting) { _ in + .sheet(unwrapping: $store.appRouteState.route, case: /AppRouteReducer.Route.setting) { _ in SettingView( store: store.scope(state: \.settingState, action: \.setting), - blurRadius: viewStore.appLockState.blurRadius + blurRadius: store.appLockState.blurRadius ) - .accentColor(viewStore.settingState.setting.accentColor) - .autoBlur(radius: viewStore.appLockState.blurRadius) + .accentColor(store.settingState.setting.accentColor) + .autoBlur(radius: store.appLockState.blurRadius) } - .sheet(unwrapping: viewStore.$appRouteState.route, case: /AppRouteReducer.Route.detail) { route in + .sheet(unwrapping: $store.appRouteState.route, case: /AppRouteReducer.Route.detail) { route in NavigationView { DetailView( store: store.scope( - state: \.appRouteState.detailState, + state: \.appRouteState.detailState.wrappedValue!, action: \.appRoute.detail ), - gid: route.wrappedValue, user: viewStore.settingState.user, - setting: viewStore.$settingState.setting, - blurRadius: viewStore.appLockState.blurRadius, - tagTranslator: viewStore.settingState.tagTranslator + gid: route.wrappedValue, user: store.settingState.user, + setting: $store.settingState.setting, + blurRadius: store.appLockState.blurRadius, + tagTranslator: store.settingState.tagTranslator ) } - .accentColor(viewStore.settingState.setting.accentColor) - .autoBlur(radius: viewStore.appLockState.blurRadius) + .accentColor(store.settingState.setting.accentColor) + .autoBlur(radius: store.appLockState.blurRadius) .environment(\.inSheet, true) .navigationViewStyle(.stack) } .progressHUD( - config: viewStore.appRouteState.hudConfig, - unwrapping: viewStore.$appRouteState.route, + config: store.appRouteState.hudConfig, + unwrapping: $store.appRouteState.route, case: /AppRouteReducer.Route.hud ) - .onChange(of: scenePhase) { _, newValue in viewStore.send(.onScenePhaseChange(newValue)) } - .onOpenURL { viewStore.send(.appRoute(.handleDeepLink($0))) } + .onChange(of: scenePhase) { _, newValue in store.send(.onScenePhaseChange(newValue)) } + .onOpenURL { store.send(.appRoute(.handleDeepLink($0))) } } }