Created
June 18, 2023 10:21
-
-
Save satishVekariya/c86e176abcb058d9a149848bc844327d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fileprivate struct KeyboardToolBar: ViewModifier { | |
let focusableIds: [String] | |
@State private var activeId: String? = nil | |
func body(content: Content) -> some View { | |
content | |
.toolbar { | |
ToolbarItemGroup(placement: .keyboard) { | |
if focusableIds.count > 1 { | |
Button { | |
if let id = activeId, let beforeId = focusableIds.item(before: id) { | |
NotificationCenter.default.post(name: .fieldWillBecomeActive, object: beforeId) | |
} | |
} label: { | |
Image(systemName: "chevron.up") | |
} | |
.disabled(activeId == focusableIds.first) | |
.accessibilityIdentifier("KeyboardToolBar_UpButton") | |
Button { | |
if let id = activeId, let afterId = focusableIds.item(after: id) { | |
NotificationCenter.default.post(name: .fieldWillBecomeActive, object: afterId) | |
} | |
} label: { | |
Image(systemName: "chevron.down") | |
} | |
.disabled(activeId == focusableIds.last) | |
.accessibilityIdentifier("KeyboardToolBar_DownButton") | |
} | |
Spacer() | |
Button("Done") { | |
if let activeId { | |
NotificationCenter.default.post(name: .fieldWillResignActive, object: activeId) | |
} | |
} | |
.accessibilityIdentifier("KeyboardToolBar_DoneButton") | |
} | |
} | |
.onFieldDidBecomeActive(assign: $activeId) | |
} | |
} | |
fileprivate extension Notification.Name { | |
static let fieldDidBecomeActive = Notification.Name("fieldDidBecomeActive") | |
static let fieldWillBecomeActive = Notification.Name("fieldWillBecomeActive") | |
static let fieldWillResignActive = Notification.Name("fieldWillResignActive") | |
} | |
fileprivate struct KeyboardFocusTarget: ViewModifier { | |
let focus: Binding<Bool> | |
let id: String | |
func body(content: Content) -> some View { | |
content | |
.onChange(of: focus.wrappedValue) { newValue in | |
if newValue == true { | |
NotificationCenter.default.post(name: .fieldDidBecomeActive, object: id) | |
} | |
} | |
.onFieldWillBecomeActive(id: id) { | |
focus.wrappedValue = true | |
} | |
.onFieldWillResignActive(id: id) { | |
focus.wrappedValue = false | |
} | |
} | |
} | |
public protocol KeyboardFocusable { | |
/// A unique view's identifier | |
var id: String { get } | |
} | |
public extension View { | |
/// Observe and store field's focus state into given binding | |
/// | |
/// Use this modifier on views(fields) those are interested in handling keyboard's toolbar base focus. | |
/// | |
/// - Parameters: | |
/// - focus: Binding to store field's focus state | |
/// - id: View's id | |
/// - Returns: SwiftUI's `some View` | |
func fieldFocus(_ focus: Binding<Bool>, id: String) -> some View { | |
modifier(KeyboardFocusTarget(focus: focus, id: id)) | |
} | |
/// Observe and store field's focus state into given binding | |
/// | |
/// Use this modifier on views(fields) those are interested in handling keyboard's toolbar base focus. | |
/// | |
/// - Parameters: | |
/// - focus: Binding to store field's focus state | |
/// - id: View's id | |
/// - Returns: SwiftUI's `some View` | |
func fieldFocus(_ focus: FocusState<Bool>.Binding, id: String) -> some View { | |
let binding = Binding(get: { focus.wrappedValue }, set: { newVal in focus.wrappedValue = newVal }) | |
return modifier(KeyboardFocusTarget(focus: binding, id: id)) | |
} | |
/// A modifier that will create keyboard's tool bar view and handle focus switching for given list of view ids | |
/// - Parameter ids: List of view's ids | |
/// - Returns: A view that listen and triggers focus shifting action when user tap on keyboard toolbar button. | |
func keyboardToolbarFocusable(ids: [String]) -> some View { | |
modifier(KeyboardToolBar(focusableIds: ids)) | |
} | |
/// Listen field will become active event for given view's id. | |
/// - Parameters: | |
/// - id: View's id | |
/// - perform: An action will performed on resign event. | |
/// - Returns: SwiftUI's `some View` | |
func onFieldWillBecomeActive<FieldId: Equatable>(id: FieldId, perform: @escaping () -> Void) -> some View { | |
onReceive(NotificationCenter.default.publisher(for: .fieldWillBecomeActive)) { n in | |
if let fieldId = n.object as? FieldId, fieldId == id { | |
perform() | |
} | |
} | |
} | |
} | |
fileprivate extension View { | |
/// Listen field resign event for given view's id. | |
/// - Parameters: | |
/// - id: View's id | |
/// - perform: An action will performed on resign event. | |
/// - Returns: SwiftUI's `some View` | |
func onFieldWillResignActive<FieldId: Equatable>(id: FieldId, perform: @escaping () -> Void) -> some View { | |
onReceive(NotificationCenter.default.publisher(for: .fieldWillResignActive)) { n in | |
if let fieldId = n.object as? FieldId, fieldId == id { | |
perform() | |
} | |
} | |
} | |
/// Listen field did become active event and set active field's id into given binding. | |
/// - Parameters: | |
/// - assign: An binding to store/assign active field's id. | |
/// - Returns: SwiftUI's `some View` | |
func onFieldDidBecomeActive<FieldId>(assign: Binding<FieldId?>) -> some View { | |
onReceive(NotificationCenter.default.publisher(for: .fieldDidBecomeActive)) { n in | |
if let fieldId = n.object as? FieldId { | |
assign.wrappedValue = fieldId | |
} | |
} | |
} | |
} | |
// MARK: - Helper extension | |
public extension Collection where Iterator.Element: Equatable { | |
typealias Item = Self.Iterator.Element | |
func safeIndex(after index: Index) -> Index? { | |
let nextIndex = self.index(after: index) | |
return (nextIndex < self.endIndex) ? nextIndex : nil | |
} | |
func index(afterWithWrapAround index: Index) -> Index { | |
return self.safeIndex(after: index) ?? self.startIndex | |
} | |
func item(after item: Item) -> Item? { | |
return self.firstIndex(of: item) | |
.flatMap(self.safeIndex(after:)) | |
.map{ self[$0] } | |
} | |
func item(afterWithWrapAround item: Item) -> Item? { | |
return self.firstIndex(of: item) | |
.map(self.index(afterWithWrapAround:)) | |
.map{ self[$0] } | |
} | |
} | |
public extension BidirectionalCollection where Iterator.Element: Equatable { | |
typealias Item = Self.Iterator.Element | |
func safeIndex(before index: Index) -> Index? { | |
let previousIndex = self.index(before: index) | |
return (self.startIndex <= previousIndex) ? previousIndex : nil | |
} | |
func index(beforeWithWrapAround index: Index) -> Index { | |
return self.safeIndex(before: index) ?? self.index(before: self.endIndex) | |
} | |
func item(before item: Item) -> Item? { | |
return self.firstIndex(of: item) | |
.flatMap(self.safeIndex(before:)) | |
.map{ self[$0] } | |
} | |
func item(beforeWithWrapAround item: Item) -> Item? { | |
return self.firstIndex(of: item) | |
.map(self.index(beforeWithWrapAround:)) | |
.map{ self[$0] } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment