diff --git a/Modules/Sources/ForumFeature/ForumScreen.swift b/Modules/Sources/ForumFeature/ForumScreen.swift index dd107973..79830f5f 100644 --- a/Modules/Sources/ForumFeature/ForumScreen.swift +++ b/Modules/Sources/ForumFeature/ForumScreen.swift @@ -46,6 +46,7 @@ public struct ForumScreen: View { } } .scrollContentBackground(.hidden) + .scrollDismissesKeyboard(.immediately) .refreshable { await store.send(.onRefresh).finish() } diff --git a/Modules/Sources/PageNavigationFeature/PageNavigationFeature.swift b/Modules/Sources/PageNavigationFeature/PageNavigationFeature.swift index 4d154f0e..5b0bd121 100644 --- a/Modules/Sources/PageNavigationFeature/PageNavigationFeature.swift +++ b/Modules/Sources/PageNavigationFeature/PageNavigationFeature.swift @@ -22,14 +22,20 @@ public struct PageNavigationFeature: Reducer, Sendable { public init() {} + // MARK: - State + @ObservableState public struct State: Equatable { + public enum Field: Sendable { case page } + @Shared(.appSettings) var appSettings: AppSettings let type: PageNavigationType - public var count: Int = 0 - public var offset: Int = 0 + public var page = "1" + public var count = 0 + public var offset = 0 var perPage: Int + public var focus: Field? public var shouldShow: Bool { return count > perPage } @@ -59,27 +65,65 @@ public struct PageNavigationFeature: Reducer, Sendable { } } - public enum Action { + // MARK: - Action + + public enum Action: BindableAction { + case binding(BindingAction) case firstPageTapped case previousPageTapped case nextPageTapped case lastPageTapped + case doneButtonTapped + case onViewTapped + case goToPage(newPage: Int) case update(count: Int, offset: Int?) case offsetChanged(to: Int) } + // MARK: - Body + public var body: some Reducer { + BindingReducer() + Reduce { state, action in switch action { + case .binding(\.focus): + state.page = String(state.currentPage) + return .none + + case .binding: + return .none + + case .doneButtonTapped: + state.focus = nil + guard let newPage = Int(state.page) else { + state.page = String(state.currentPage) + return .none + } + guard newPage != state.currentPage else { + return .none + } + return .send(.goToPage(newPage: newPage)) + + case .goToPage(let newPage): + state.offset = (newPage - 1) * state.perPage + + case .onViewTapped: + state.focus = state.focus == nil ? .page : nil + return .none + case .firstPageTapped: state.offset = 0 + state.page = String(state.currentPage) case .previousPageTapped: state.offset -= state.perPage + state.page = String(state.currentPage) case .nextPageTapped: state.offset += state.perPage + state.page = String(state.currentPage) case .lastPageTapped: let targetOffset = state.count - (state.count % state.perPage) @@ -88,6 +132,7 @@ public struct PageNavigationFeature: Reducer, Sendable { } else { state.offset = targetOffset } + state.page = String(state.currentPage) case let .update(count: count, offset: offset): state.count = count diff --git a/Modules/Sources/PageNavigationFeature/PageNavigationView.swift b/Modules/Sources/PageNavigationFeature/PageNavigationView.swift index c14dda5a..61f44d4e 100644 --- a/Modules/Sources/PageNavigationFeature/PageNavigationView.swift +++ b/Modules/Sources/PageNavigationFeature/PageNavigationView.swift @@ -10,12 +10,14 @@ import ComposableArchitecture import SFSafeSymbols import Models import SharedUI +import Perception public struct PageNavigation: View { // MARK: - Properties - public var store: StoreOf + @Perception.Bindable public var store: StoreOf + @FocusState private var focus: PageNavigationFeature.State.Field? // MARK: - Init @@ -71,20 +73,55 @@ public struct PageNavigation: View { .disabled(store.currentPage + 1 > store.totalPages) } .overlay { - Text(String("\(store.currentPage) / \(store.totalPages)")) - .font(.subheadline) - .foregroundStyle(Color(.Labels.secondary)) - .padding(.vertical, 6) - .padding(.horizontal, 12) - .background( - RoundedRectangle(cornerRadius: 8) - .foregroundStyle(Color(.Background.teritary)) - ) - .contentTransition(.numericText()) + HStack(spacing: 8) { + TextField(String(""), text: $store.page) + .font(.subheadline) + .keyboardType(.numberPad) + .focused($focus, equals: PageNavigationFeature.State.Field.page) + .fixedSize() + .onChange(of: store.page) { newValue in + guard !store.page.isEmpty else { return } + store.page = String(min(Int(store.page.filter(\.isNumber))!, store.totalPages)) + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + + Button { + store.send(.doneButtonTapped) + } label: { + Text("Done", bundle: .module) + } + } + } + .opacity(store.focus == nil ? 0 : 1) + .overlay { + Text(store.page) + .font(.subheadline) + .fixedSize() + .opacity(store.focus == nil ? 1 : 0) + .contentTransition(.numericText()) + .animation(.default, value: store.page) + } + + Text(verbatim: "/") + .font(.subheadline) + + Text(verbatim: "\(store.totalPages)") + .font(.subheadline) + } + .padding(.vertical, 6) + .padding(.horizontal, 12) + .background( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(Color(.Background.teritary)) + ) + .onTapGesture { + store.send(.onViewTapped) + } } + .bind($store.focus, to: $focus) .listRowSeparator(.hidden) - .animation(.default, value: store.currentPage) - .frame(maxWidth: .infinity, maxHeight: 32) } // MARK: - Navigation Arrow diff --git a/Modules/Sources/PageNavigationFeature/Resources/Localizable.xcstrings b/Modules/Sources/PageNavigationFeature/Resources/Localizable.xcstrings new file mode 100644 index 00000000..db303ae0 --- /dev/null +++ b/Modules/Sources/PageNavigationFeature/Resources/Localizable.xcstrings @@ -0,0 +1,16 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "Done" : { + "localizations" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Готово" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Modules/Sources/TopicFeature/TopicScreen.swift b/Modules/Sources/TopicFeature/TopicScreen.swift index c94f182a..5dd3a7dc 100644 --- a/Modules/Sources/TopicFeature/TopicScreen.swift +++ b/Modules/Sources/TopicFeature/TopicScreen.swift @@ -52,6 +52,7 @@ public struct TopicScreen: View { } } } + .scrollDismissesKeyboard(.immediately) } } .overlay {