Skip to content

Commit 3133026

Browse files
committed
added focusable Modifier & AppKit implementation for all views in ControlsExample
1 parent 6ddacb9 commit 3133026

File tree

4 files changed

+99
-22
lines changed

4 files changed

+99
-22
lines changed

Examples/Sources/ControlsExample/ControlsApp.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ struct ControlsApp: App {
1616
@State var text = ""
1717
@State var flavor: String? = nil
1818
@State var enabled = true
19+
@State var isFocusable = true
1920

2021
var body: some Scene {
2122
WindowGroup("ControlsApp") {
@@ -78,10 +79,20 @@ struct ControlsApp: App {
7879
Text("You chose: \(flavor ?? "Nothing yet!")")
7980
}
8081
}.padding().disabled(!enabled)
82+
.focusable(isFocusable)
8183

8284
Toggle(enabled ? "Disable all" : "Enable all", active: $enabled)
8385
.padding()
86+
.focusable(isFocusable)
87+
88+
Toggle(
89+
isFocusable ? "Disable focusability for all" : "Enable focusability for all",
90+
active: $isFocusable
91+
)
92+
.padding()
93+
.focusable(isFocusable)
8494
}
95+
8596
}.defaultSize(width: 400, height: 600)
8697
}
8798
}

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ public final class AppKitBackend: AppBackend {
576576
}
577577

578578
public func createButton() -> Widget {
579-
return NSButton(title: "", target: nil, action: nil)
579+
return NSCustomButton(title: "", target: nil, action: nil)
580580
}
581581

582582
public func updateButton(
@@ -585,43 +585,45 @@ public final class AppKitBackend: AppBackend {
585585
environment: EnvironmentValues,
586586
action: @escaping () -> Void
587587
) {
588-
let button = button as! NSButton
588+
let button = button as! NSCustomButton
589589
button.attributedTitle = Self.attributedString(
590590
for: label,
591591
in: environment.with(\.multilineTextAlignment, .center)
592592
)
593593
button.bezelStyle = .regularSquare
594594
button.appearance = environment.colorScheme.nsAppearance
595595
button.isEnabled = environment.isEnabled
596+
button.canBeFocused = environment.isFocusable
596597
button.onAction = { _ in
597598
action()
598599
}
599600
}
600601

601602
public func createSwitch() -> Widget {
602-
return NSSwitch()
603+
return NSCustomSwitch()
603604
}
604605

605606
public func updateSwitch(
606607
_ toggleSwitch: Widget,
607608
environment: EnvironmentValues,
608609
onChange: @escaping (Bool) -> Void
609610
) {
610-
let toggleSwitch = toggleSwitch as! NSSwitch
611+
let toggleSwitch = toggleSwitch as! NSCustomSwitch
611612
toggleSwitch.isEnabled = environment.isEnabled
613+
toggleSwitch.canBeFocused = environment.isFocusable
612614
toggleSwitch.onAction = { toggleSwitch in
613-
let toggleSwitch = toggleSwitch as! NSSwitch
615+
let toggleSwitch = toggleSwitch as! NSCustomSwitch
614616
onChange(toggleSwitch.state == .on)
615617
}
616618
}
617619

618620
public func setState(ofSwitch toggleSwitch: Widget, to state: Bool) {
619-
let toggleSwitch = toggleSwitch as! NSSwitch
621+
let toggleSwitch = toggleSwitch as! NSCustomSwitch
620622
toggleSwitch.state = state ? .on : .off
621623
}
622624

623625
public func createToggle() -> Widget {
624-
let toggle = NSButton()
626+
let toggle = NSCustomButton()
625627
toggle.setButtonType(.pushOnPushOff)
626628
return toggle
627629
}
@@ -632,47 +634,49 @@ public final class AppKitBackend: AppBackend {
632634
environment: EnvironmentValues,
633635
onChange: @escaping (Bool) -> Void
634636
) {
635-
let toggle = toggle as! NSButton
637+
let toggle = toggle as! NSCustomButton
636638
toggle.attributedTitle = Self.attributedString(
637639
for: label,
638640
in: environment.with(\.multilineTextAlignment, .center)
639641
)
640642
toggle.isEnabled = environment.isEnabled
643+
toggle.canBeFocused = environment.isFocusable
641644
toggle.onAction = { toggle in
642-
let toggle = toggle as! NSButton
645+
let toggle = toggle as! NSCustomButton
643646
onChange(toggle.state == .on)
644647
}
645648
}
646649

647650
public func setState(ofToggle toggle: Widget, to state: Bool) {
648-
let toggle = toggle as! NSButton
651+
let toggle = toggle as! NSCustomButton
649652
toggle.state = state ? .on : .off
650653
}
651654

652655
public func createCheckbox() -> Widget {
653-
NSButton(checkboxWithTitle: "", target: nil, action: nil)
656+
NSCustomButton(checkboxWithTitle: "", target: nil, action: nil)
654657
}
655658

656659
public func updateCheckbox(
657660
_ checkbox: Widget,
658661
environment: EnvironmentValues,
659662
onChange: @escaping (Bool) -> Void
660663
) {
661-
let checkbox = checkbox as! NSButton
664+
let checkbox = checkbox as! NSCustomButton
662665
checkbox.isEnabled = environment.isEnabled
666+
checkbox.canBeFocused = environment.isFocusable
663667
checkbox.onAction = { toggle in
664-
let checkbox = toggle as! NSButton
668+
let checkbox = toggle as! NSCustomButton
665669
onChange(checkbox.state == .on)
666670
}
667671
}
668672

669673
public func setState(ofCheckbox checkbox: Widget, to state: Bool) {
670-
let toggle = checkbox as! NSButton
674+
let toggle = checkbox as! NSCustomButton
671675
toggle.state = state ? .on : .off
672676
}
673677

674678
public func createSlider() -> Widget {
675-
return NSSlider()
679+
return NSCustomSlider()
676680
}
677681

678682
public func updateSlider(
@@ -684,23 +688,24 @@ public final class AppKitBackend: AppBackend {
684688
onChange: @escaping (Double) -> Void
685689
) {
686690
// TODO: Implement decimalPlaces
687-
let slider = slider as! NSSlider
691+
let slider = slider as! NSCustomSlider
688692
slider.minValue = minimum
689693
slider.maxValue = maximum
690694
slider.onAction = { slider in
691-
let slider = slider as! NSSlider
695+
let slider = slider as! NSCustomSlider
692696
onChange(slider.doubleValue)
693697
}
698+
slider.canBeFocused = environment.isFocusable
694699
slider.isEnabled = environment.isEnabled
695700
}
696701

697702
public func setValue(ofSlider slider: Widget, to value: Double) {
698-
let slider = slider as! NSSlider
703+
let slider = slider as! NSCustomSlider
699704
slider.doubleValue = value
700705
}
701706

702707
public func createPicker() -> Widget {
703-
return NSPopUpButton()
708+
return NSCustomPopUpButton()
704709
}
705710

706711
public func updatePicker(
@@ -709,7 +714,7 @@ public final class AppKitBackend: AppBackend {
709714
environment: EnvironmentValues,
710715
onChange: @escaping (Int?) -> Void
711716
) {
712-
let picker = picker as! NSPopUpButton
717+
let picker = picker as! NSCustomPopUpButton
713718
picker.isEnabled = environment.isEnabled
714719
picker.menu?.removeAllItems()
715720
for option in options {
@@ -718,14 +723,15 @@ public final class AppKitBackend: AppBackend {
718723
picker.menu?.addItem(item)
719724
}
720725
picker.onAction = { picker in
721-
let picker = picker as! NSPopUpButton
726+
let picker = picker as! NSCustomPopUpButton
722727
onChange(picker.indexOfSelectedItem)
723728
}
729+
picker.canBeFocused = environment.isFocusable
724730
picker.bezelStyle = .regularSquare
725731
}
726732

727733
public func setSelectedOption(ofPicker picker: Widget, to selectedOption: Int?) {
728-
let picker = picker as! NSPopUpButton
734+
let picker = picker as! NSCustomPopUpButton
729735
if let index = selectedOption {
730736
picker.selectItem(at: index)
731737
} else {
@@ -760,6 +766,7 @@ public final class AppKitBackend: AppBackend {
760766
onChange(textField.stringValue)
761767
}
762768
textField.onSubmit = onSubmit
769+
textField.canBeFocused = environment.isFocusable
763770

764771
if #available(macOS 14, *) {
765772
textField.contentType =
@@ -813,6 +820,7 @@ public final class AppKitBackend: AppBackend {
813820
}
814821
textEditor.appearance = environment.colorScheme.nsAppearance
815822
textEditor.isEditable = environment.isEnabled
823+
textEditor.canBeFocused = environment.isFocusable
816824

817825
if #available(macOS 14, *) {
818826
textEditor.contentType =
@@ -1963,11 +1971,49 @@ final class ObjectAssociation<T: Any> {
19631971
}
19641972
}
19651973

1974+
final class NSCustomButton: NSButton {
1975+
var canBeFocused: Bool = true
1976+
1977+
override var acceptsFirstResponder: Bool {
1978+
return canBeFocused
1979+
}
1980+
}
1981+
1982+
final class NSCustomSlider: NSSlider {
1983+
var canBeFocused: Bool = true
1984+
1985+
override var acceptsFirstResponder: Bool {
1986+
return canBeFocused
1987+
}
1988+
}
1989+
1990+
final class NSCustomSwitch: NSSwitch {
1991+
var canBeFocused: Bool = true
1992+
1993+
override var acceptsFirstResponder: Bool {
1994+
return canBeFocused
1995+
}
1996+
}
1997+
1998+
final class NSCustomPopUpButton: NSPopUpButton {
1999+
var canBeFocused: Bool = true
2000+
2001+
override var acceptsFirstResponder: Bool {
2002+
return canBeFocused
2003+
}
2004+
}
2005+
19662006
class NSObservableTextField: NSTextField {
19672007
override func textDidChange(_ notification: Notification) {
19682008
onEdit?(self)
19692009
}
19702010

2011+
var canBeFocused: Bool = true
2012+
2013+
override var acceptsFirstResponder: Bool {
2014+
return canBeFocused
2015+
}
2016+
19712017
var onEdit: ((NSTextField) -> Void)?
19722018
var _onSubmitAction = Action({})
19732019
var onSubmit: () -> Void {
@@ -1987,6 +2033,12 @@ class NSObservableTextView: NSTextView, NSTextViewDelegate {
19872033
onEdit?(self)
19882034
}
19892035

2036+
var canBeFocused: Bool = true
2037+
2038+
override var acceptsFirstResponder: Bool {
2039+
return canBeFocused
2040+
}
2041+
19902042
var onEdit: ((NSTextView) -> Void)?
19912043
}
19922044

Sources/SwiftCrossUI/Environment/EnvironmentValues.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ public struct EnvironmentValues {
9595
/// The style of toggle to use.
9696
public var toggleStyle: ToggleStyle
9797

98+
/// Wether a View is focusable
99+
public var isFocusable: Bool
100+
98101
// Backing storage for extensible subscript
99102
private var extraValues: [ObjectIdentifier: Any]
100103

@@ -208,6 +211,7 @@ public struct EnvironmentValues {
208211
toggleStyle = .button
209212
isEnabled = true
210213
scrollDismissesKeyboardMode = .automatic
214+
isFocusable = true
211215
}
212216

213217
/// Returns a copy of the environment with the specified property set to the
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
extension View {
2+
/// Set wether a View is focusable
3+
/// Only has effect on out of the box interactable Views
4+
/// doesn't work on Views using onTapGesture instead of Button
5+
public func focusable(_ isFocusable: Bool = true) -> some View {
6+
EnvironmentModifier(self) { environment in
7+
environment.with(\.isFocusable, isFocusable)
8+
}
9+
}
10+
}

0 commit comments

Comments
 (0)