Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AppKitNavigation - Bindings #239

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
8 changes: 7 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"),
.package(url: "https://github.com/pointfreeco/swift-perception", from: "1.3.4"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"),
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.1.0"),
],
targets: [
.target(
Expand Down Expand Up @@ -82,9 +83,14 @@ let package = Package(
.target(
name: "AppKitNavigation",
dependencies: [
"SwiftNavigation"
"SwiftNavigation",
"AppKitNavigationShim",
.product(name: "IdentifiedCollections", package: "swift-identified-collections"),
]
),
.target(
name: "AppKitNavigationShim"
),
.testTarget(
name: "UIKitNavigationTests",
dependencies: [
Expand Down
6 changes: 6 additions & 0 deletions Package@swift-6.0.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"),
.package(url: "https://github.com/pointfreeco/swift-perception", from: "1.3.4"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"),
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.1.0"),
],
targets: [
.target(
Expand Down Expand Up @@ -83,8 +84,13 @@ let package = Package(
name: "AppKitNavigation",
dependencies: [
"SwiftNavigation",
"AppKitNavigationShim",
.product(name: "IdentifiedCollections", package: "swift-identified-collections"),
]
),
.target(
name: "AppKitNavigationShim"
),
.testTarget(
name: "UIKitNavigationTests",
dependencies: [
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppKitNavigation/AppKitAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
return try result!._rethrowGet()

case let .swiftUI(animation):
var result: Swift.Result<Result, Error>?
#if swift(>=6)
var result: Swift.Result<Result, Error>?
if #available(macOS 15, *) {
NSAnimationContext.animate(animation) {
result = Swift.Result(catching: body)
Expand Down
48 changes: 48 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSAlert.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
import AppKit

extension NSAlert {
/// Creates and returns a alert for displaying an alert using a data description.
///
/// - Parameters:
/// - state: A data description of the alert.
/// - handler: A closure that is invoked with an action held in `state`.
public convenience init<Action>(
state: AlertState<Action>,
handler: @escaping (_ action: Action?) -> Void
) {
self.init()
self.messageText = String(state: state.title)
state.message.map { self.informativeText = String(state: $0) }

for button in state.buttons {
addButton(button, action: handler)
}
}
}

extension NSAlert {
public func addButton<Action>(
_ buttonState: ButtonState<Action>,
action handler: @escaping (_ action: Action?) -> Void = { (_: Never?) in }
) {
let button = addButton(withTitle: String(state: buttonState.label))

button.createActionProxyIfNeeded().addBindingAction { _ in
buttonState.withAction(handler)
}

if buttonState.role == .destructive, #available(macOS 11.0, *) {
button.hasDestructiveAction = true
}

if buttonState.role == .cancel {
button.keyEquivalent = "\u{1b}"
}

if #available(macOS 12, *) {
button.setAccessibilityLabel(buttonState.label.accessibilityLabel.map { String(state: $0) })
}
}
}
#endif
42 changes: 42 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSColorPanel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)

import AppKit

extension NSColorPanel: @retroactive Sendable {}
extension NSColorPanel: TargetActionProtocol {
public var target: AnyObject? {
set { setTarget(newValue) }
get { value(forKeyPath: "target") as? AnyObject }
}

public var action: Selector? {
set { setAction(newValue) }
get { value(forKeyPath: "action") as? Selector }
}
}

extension NSColorPanel {
/// Creates a new color panel and registers the binding against the
/// selected color.
///
/// - Parameters:
/// - frame: The frame rectangle for the view, measured in points.
/// - color: The binding to read from for the selected color, and write to when the
/// selected color is changes.
public convenience init(color: UIBinding<NSColor>) {
self.init()
bind(color: color)
}

/// Establishes a two-way connection between a binding and the color panel's selected color.
///
/// - Parameter color: The binding to read from for the selected color, and write to
/// when the selected color changes.
/// - Returns: A cancel token.
@discardableResult
public func bind(color: UIBinding<NSColor>) -> ObserveToken {
bind(color, to: \.color)
}
}

#endif
27 changes: 27 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSColorWell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
import AppKit

extension NSColorWell {
/// Creates a new color well with the specified frame and registers the binding against the
/// selected color.
///
/// - Parameters:
/// - frame: The frame rectangle for the view, measured in points.
/// - color: The binding to read from for the selected color, and write to when the
/// selected color is changes.
public convenience init(frame: CGRect = .zero, color: UIBinding<NSColor>) {
self.init(frame: frame)
bind(color: color)
}

/// Establishes a two-way connection between a binding and the color well's selected color.
///
/// - Parameter color: The binding to read from for the selected color, and write to
/// when the selected color changes.
/// - Returns: A cancel token.
@discardableResult
public func bind(color: UIBinding<NSColor>) -> ObserveToken {
bind(color, to: \.color)
}
}
#endif
34 changes: 34 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSControl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)

import AppKit

extension NSControl: @retroactive Sendable {}
extension NSControl: TargetActionProtocol {}

extension NSControl {
public convenience init(action: @escaping (Self) -> Void) {
self.init(frame: .zero)
createActionProxyIfNeeded().addAction { [weak self] _ in
guard let self else { return }
action(self)
}
}

@discardableResult
public func addAction(_ action: @escaping (NSControl) -> Void) -> UUID {
createActionProxyIfNeeded().addAction { [weak self] _ in
guard let self else { return }
action(self)
}
}

public func removeAction(for id: UUID) {
createActionProxyIfNeeded().removeAction(for: id)
}

public func removeAllActions() {
createActionProxyIfNeeded().removeAllActions()
}
}

#endif
27 changes: 27 additions & 0 deletions Sources/AppKitNavigation/Bindings/NSDatePicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
import AppKit

extension NSDatePicker {
/// Creates a new date picker with the specified frame and registers the binding against the
/// selected date.
///
/// - Parameters:
/// - frame: The frame rectangle for the view, measured in points.
/// - date: The binding to read from for the selected date, and write to when the selected
/// date changes.
public convenience init(frame: CGRect = .zero, date: UIBinding<Date>) {
self.init(frame: frame)
bind(date: date)
}

/// Establishes a two-way connection between a binding and the date picker's selected date.
///
/// - Parameter date: The binding to read from for the selected date, and write to when the
/// selected date changes.
/// - Returns: A cancel token.
@discardableResult
public func bind(date: UIBinding<Date>) -> ObserveToken {
bind(date, to: \.dateValue)
}
}
#endif
Loading
Loading