Skip to content

Instantly share code, notes, and snippets.

@satishVekariya
Created June 18, 2023 10:21
Show Gist options
  • Save satishVekariya/c86e176abcb058d9a149848bc844327d to your computer and use it in GitHub Desktop.
Save satishVekariya/c86e176abcb058d9a149848bc844327d to your computer and use it in GitHub Desktop.
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