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 #200

Draft
wants to merge 39 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
37e7adb
Initial commit
Mx-Iris Aug 13, 2024
4678f0a
Updates
Mx-Iris Aug 13, 2024
4611a2a
Updates
Mx-Iris Aug 14, 2024
06f09af
Updates
Mx-Iris Aug 14, 2024
590bb2b
WIP
Mx-Iris Aug 15, 2024
cb5aa02
WIP
Mx-Iris Aug 15, 2024
26ef0fa
Refactoring, can't compile for now
Mx-Iris Aug 16, 2024
8e0e225
WIP
Mx-Iris Aug 16, 2024
01fafbe
WIP
Mx-Iris Aug 16, 2024
cd4422f
WIP-Fix bugs and support NSSave/OpenPanel
Mx-Iris Aug 16, 2024
27c6152
WIP-Support WiFiFeature Case Study
Mx-Iris Aug 18, 2024
7278ccc
WIP
Mx-Iris Aug 18, 2024
61e1fff
WIP
Mx-Iris Aug 18, 2024
2ba32eb
Format
Mx-Iris Aug 19, 2024
e2c5db4
Common
Mx-Iris Aug 19, 2024
0c61b63
Remove unused code
Mx-Iris Aug 20, 2024
c2bdb0d
Remove unused code
Mx-Iris Aug 20, 2024
82c7737
Merge branch 'main' into appkit-navigation-common
stephencelis Aug 22, 2024
8e5e4e6
Integrate custom transaction
stephencelis Aug 22, 2024
2b91081
address fatal error
stephencelis Aug 22, 2024
cad947b
Round out animation
stephencelis Aug 23, 2024
e863c54
Update Package.swift
mbrandonw Aug 23, 2024
00ed18a
Update Package@swift-6.0.swift
mbrandonw Aug 23, 2024
df6eda7
Support AppKit Navigation
Mx-Iris Aug 24, 2024
04b6e54
Merge branch 'main' into appkit-navigation-navigation
Mx-Iris Aug 27, 2024
2228ad1
Update Sources/AppKitNavigationShim/include/shim.h
Mx-Iris Aug 27, 2024
47ac7b3
Update Sources/AppKitNavigation/Navigation/Sheet.swift
Mx-Iris Aug 27, 2024
6111e32
Update Sources/AppKitNavigation/Navigation/ModalSessionContent.swift
Mx-Iris Aug 27, 2024
3832d51
Fixes building errors
Mx-Iris Sep 17, 2024
8485f16
Merge remote-tracking branch 'upstream/main' into appkit-navigation-b…
Mx-Iris Oct 11, 2024
593cae8
Support control bindings
Mx-Iris Oct 11, 2024
2479dba
Translate some protocol to internal
Mx-Iris Oct 11, 2024
863c8d2
Completion
Mx-Iris Oct 11, 2024
2963dd1
Fixes
Mx-Iris Oct 11, 2024
9d0df7e
Fix errors
Mx-Iris Nov 30, 2024
6dbe303
Merge remote-tracking branch 'origin/appkit-navigation-navigation'
Mx-Iris Feb 4, 2025
ddd9af0
Merge remote-tracking branch 'origin/appkit-navigation-bindings'
Mx-Iris Feb 4, 2025
7f867ac
Merge remote-tracking branch 'upstream/main'
Mx-Iris Feb 4, 2025
567a607
Merge upstream main branch
Mx-Iris Feb 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
import SwiftUI
import AppKit
import AppKitNavigation

class BasicsNavigationViewController: XiblessViewController<NSView>, AppKitCaseStudy {
let caseStudyTitle = "Basics"
let readMe = """
This case study demonstrates how to perform every major form of navigation in UIKit (alerts, \
sheets, drill-downs) by driving navigation off of optional and boolean state.
"""
@UIBindable var model = Model()

override func viewDidLoad() {
super.viewDidLoad()

let showAlertButton = NSButton { [weak self] _ in
self?.model.alert = "Hello!"
}

let showSheetButton = NSButton { [weak self] _ in
self?.model.sheet = .random(in: 1 ... 1_000)
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self?.model.sheet = nil
}
}

let showSheetFromBooleanButton = NSButton { [weak self] _ in
self?.model.isSheetPresented = true
}

let stack = NSStackView(views: [
showAlertButton,
showSheetButton,
showSheetFromBooleanButton,
])
stack.orientation = .vertical
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(stack)
NSLayoutConstraint.activate([
stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])

observe { [weak self] in
guard let self else { return }

if let url = model.url {
showAlertButton.title = "URL is: \(url)"
} else {
showAlertButton.title = "Alert is presented: \(model.alert != nil ? "✅" : "❌")"
}
showSheetButton.title = "Sheet is presented: \(model.sheet != nil ? "✅" : "❌")"
showSheetFromBooleanButton.title = "Sheet is presented from boolean: \(model.isSheetPresented ? "✅" : "❌")"
}

modal(item: $model.alert, id: \.self) { [unowned self] message in
// let alert = NSAlert()
// alert.messageText = "This is an alert"
// alert.informativeText = message
// alert.addButton(withTitle: "OK")
// return alert
let openPanel = NSOpenPanel(url: $model.url)
openPanel.message = message
return openPanel
}

present(item: $model.sheet, id: \.self, style: .sheet) { count in
NSHostingController(
rootView: Form { Text(count.description) }.frame(width: 100, height: 100, alignment: .center)
)
}

present(isPresented: $model.isSheetPresented, style: .sheet) {
NSHostingController(
rootView: Form { Text("Hello!") }.frame(width: 100, height: 100, alignment: .center)
)
}
}

@Observable
class Model {
var alert: String?
var isSheetPresented = false
var sheet: Int?
var url: URL?
}
}

#Preview {
BasicsNavigationViewController()
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
import SwiftUI
import AppKit
import AppKitNavigation

class ConciseEnumNavigationViewController: XiblessViewController<NSView>, AppKitCaseStudy {
let caseStudyNavigationTitle = "Enum navigation"
let caseStudyTitle = "Concise enum navigation"
let readMe = """
This case study demonstrates how to navigate to multiple destinations from a single optional \
enum.

This allows you to be very concise with your domain modeling by having a single enum \
describe all the possible destinations you can navigate to. In the case of this demo, we have \
four cases in the enum, which means there are exactly 5 possible states, including the case \
where none are active.

If you were to instead model this domain with 4 optionals (or booleans), then you would have \
16 possible states, of which only 5 are valid. That can leak complexity into your domain \
because you can never be sure of exactly what is presented at a given time.
"""
@UIBindable var model = Model()

override func viewDidLoad() {
super.viewDidLoad()

let showAlertButton = NSButton { [weak self] _ in
self?.model.destination = .alert("Hello!")
}
let showSheetButton = NSButton { [weak self] _ in
self?.model.destination = .sheet(.random(in: 1 ... 1_000))
}
let showSheetFromBooleanButton = NSButton { [weak self] _ in
self?.model.destination = .sheetWithoutPayload
}

let stack = NSStackView(views: [
showAlertButton,
showSheetButton,
showSheetFromBooleanButton,
])
stack.orientation = .vertical
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(stack)
NSLayoutConstraint.activate([
stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])

observe { [weak self] in
guard let self else { return }

showAlertButton.title = "Alert is presented: \(model.destination?.alert != nil ? "✅" : "❌")"
showSheetButton.title = "Sheet is presented: \(model.destination?.sheet != nil ? "✅" : "❌")"
showSheetFromBooleanButton.title = "Sheet is presented from boolean: \(model.destination?.sheetWithoutPayload != nil ? "✅" : "❌")"
}

modal(item: $model.destination.alert, id: \.self) { message in
let alert = NSAlert()
alert.messageText = "This is an alert"
alert.informativeText = message
alert.addButton(withTitle: "OK")
return alert
}
present(item: $model.destination.sheet, id: \.self, style: .sheet) { [unowned self] count in

NSHostingController(
rootView: Form {
Text(count.description)
Button("Close") {
self.model.destination = nil
}
}.frame(width: 200, height: 200, alignment: .center)
)
}
present(isPresented: UIBinding($model.destination.sheetWithoutPayload), style: .sheet) { [unowned self] in
NSHostingController(
rootView: Form {
Text("Hello!")
Button("Close") {
self.model.destination = nil
}
}.frame(width: 200, height: 200, alignment: .center)
)
}
}

@Observable
class Model {
var destination: Destination?
@CasePathable
@dynamicMemberLookup
enum Destination {
case alert(String)
case drillDown(Int)
case sheet(Int)
case sheetWithoutPayload
}
}
}

#Preview {
ConciseEnumNavigationViewController()
}
#endif
117 changes: 117 additions & 0 deletions Examples/CaseStudies/AppKit/AppKit+EnumControlsViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)

import AppKit
import AppKitNavigation

class EnumControlsViewController: XiblessViewController<NSView>, AppKitCaseStudy {
let caseStudyNavigationTitle = "Enum controls"
let caseStudyTitle = "Concise enum controls"
let readMe = """
This case study demonstrates how to drive form controls from bindings to enum state. In this \
example, a single `Status` enum holds two cases:

• An integer quantity for when an item is in stock, which can drive a stepper.
• A Boolean for whether an item is on back order when it is _not_ in stock, which can drive a \
switch.

This library provides tools to chain deeper into a binding's case by applying the \
`@CasePathable` macro.
"""

@CasePathable
enum Status {
case inStock(quantity: Int)
case outOfStock(isOnBackOrder: Bool)
}

@UIBinding var status: Status = .inStock(quantity: 100)

override func viewDidLoad() {
super.viewDidLoad()

let quantityLabel = NSTextField(labelWithString: "")
let quantityStepper = NSStepper()
quantityStepper.maxValue = .infinity
let quantityStack = NSStackView(views: [
quantityLabel,
quantityStepper,
])
let outOfStockButton = NSButton { [weak self] _ in
self?.status = .outOfStock(isOnBackOrder: false)
}
outOfStockButton.title = "Out of stock"
let inStockStack = NSStackView(views: [
quantityStack,
outOfStockButton,
])
inStockStack.orientation = .vertical

let isOnBackOrderLabel = NSTextField(labelWithString: "Is on back order?")
let isOnBackOrderSwitch = NSSwitch()
let isOnBackOrderStack = NSStackView(views: [
isOnBackOrderLabel,
isOnBackOrderSwitch,
])
let backInStockButton = NSButton { [weak self] _ in
self?.status = .inStock(quantity: 100)
}

backInStockButton.title = "Back in stock!"
let outOfStockStack = NSStackView(views: [
isOnBackOrderStack,
backInStockButton,
])
outOfStockStack.orientation = .vertical

let stack = NSStackView(views: [
inStockStack,
outOfStockStack,
])
stack.orientation = .vertical
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(stack)

NSLayoutConstraint.activate([
stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])

observe { [weak self] in
guard let self else { return }

inStockStack.isHidden = !status.is(\.inStock)
outOfStockStack.isHidden = !status.is(\.outOfStock)

switch status {
case .inStock:
if let quantity = $status.inStock {
quantityLabel.stringValue = "Quantity: \(quantity.wrappedValue)"
quantityStepper.bind(value: quantity.asDouble)
}

case .outOfStock:
if let isOnBackOrder = $status.outOfStock {
isOnBackOrderSwitch.bind(isOn: isOnBackOrder)
}
}
}
}
}

@available(macOS 14.0, *)
#Preview {
EnumControlsViewController()
}


extension Int {
fileprivate var asDouble: Double {
get { Double(self) }
set { self = Int(newValue) }
}
}

#endif
Loading