Skip to content

AppKitNavigation - #200

Draft
Mx-Iris wants to merge 55 commits into
pointfreeco:mainfrom
MxIris-Library-Forks:main
Draft

AppKitNavigation#200
Mx-Iris wants to merge 55 commits into
pointfreeco:mainfrom
MxIris-Library-Forks:main

Conversation

@Mx-Iris

@Mx-Iris Mx-Iris commented Aug 14, 2024

Copy link
Copy Markdown
Contributor

Hello everyone, I love AppKit. I am very pleased to see that UIKit has such an excellent framework available.

I have also watched videos from pointfreeco, where it is mentioned more than once that domain modeling is the most important thing, and using tools can make any platform serve the model.

I spent some time porting part of the API, and most of the NSControl can be use. Next, I will add some AppKit-specific navigation, such as sheet, modal, and some macOS-specific controls, and a lot of documentation still needs to be refined, as well as example projects and tests.

This PR is not yet complete, so please do not merge it yet!

For those who are interested, you can try running the example project

@Mx-Iris Mx-Iris changed the title AppKit support AppKitNavigation Aug 14, 2024
@stephencelis
stephencelis marked this pull request as draft August 14, 2024 17:55
@stephencelis

Copy link
Copy Markdown
Member

Awesome work! We're looking forward to diving into it soon 😄 I've converted things to a draft for now, so just tap the "Ready for review" button whenever you think things are in a good place.

@stephencelis

Copy link
Copy Markdown
Member

@Mx-Iris Thanks again for your work on this! We took it for a spin and are definitely excited to incorporate things back into the library.

I think to start it would be nice to break things down into smaller PRs in order to progressively ship things. This PR is still great to leave open as a "full vision" of things that folks can take for a spin. And now we could start isolating smaller chunks of work that can land in main independently.

Some potential steps/chunks:

  1. A PR that adds the AppKit-specific observe helper and AppKit-specific UITransaction infrastructure. This could include the animations, as well, or animations could be split off into their own PR..
  2. A PR that adds the base NSControl binding infrastructure.
  3. PR(s?) that add the individual binding helpers. You could do all the simple helpers in a single PR, and any helpers that are more complex could land in their own PRs.
  4. A PR that adds the navigation helpers.

If there are any obvious ways of breaking any step down further, it's appreciated, and should make it easier to review each PR and also get each PR merged more quickly rather than have any giant PR sit around for too long.

Excited for this!

@stephencelis

Copy link
Copy Markdown
Member

Oh, and another note: let's not worry about documentation or case studies at all right now. Those are typically things we save till the end when we're close to release, and we can discuss how we may want to approach that when things are further along and everything else has landed in main.

@Mx-Iris

Mx-Iris commented Aug 15, 2024

Copy link
Copy Markdown
Contributor Author

@stephencelis I understand your point now. This branch is mainly for quickly implementing my ideas and verifying if they're effective. I'll follow your suggestion and break them down into smaller units, then submit PRs for each one individually.​​​​​​​​​​​​​​​​

Mx-Iris and others added 30 commits August 28, 2024 00:59
Co-authored-by: Stephen Celis <stephen.celis@gmail.com>
Co-authored-by: Stephen Celis <stephen.celis@gmail.com>
Co-authored-by: Stephen Celis <stephen.celis@gmail.com>
…indings

# Conflicts:
#	Package.swift
#	Sources/AppKitNavigation/AppKitAnimation.swift
#	Sources/AppKitNavigation/Internal/Exports.swift
#	Sources/AppKitNavigation/UIBinding.swift
#	Sources/AppKitNavigation/UITransaction.swift
#	Sources/SwiftNavigation/Internal/ErrorMechanism.swift
#	Sources/SwiftNavigation/Internal/ToOptionalUnit.swift
# Conflicts:
#	Package.swift
#	Package@swift-6.0.swift
#	Sources/AppKitNavigation/AppKitAnimation.swift
#	Sources/AppKitNavigation/Internal/Exports.swift
#	Sources/AppKitNavigation/Navigation/Modal.swift
#	Sources/AppKitNavigation/Navigation/ModalContent.swift
#	Sources/AppKitNavigation/Navigation/ModalSessionContent.swift
#	Sources/AppKitNavigation/Navigation/NavigationContent.swift
#	Sources/AppKitNavigation/Navigation/NavigationObserver.swift
#	Sources/AppKitNavigation/Navigation/Presentation.swift
#	Sources/AppKitNavigation/Navigation/Sheet.swift
#	Sources/AppKitNavigation/Navigation/SheetContent.swift
#	Sources/AppKitNavigation/UIBinding.swift
#	Sources/AppKitNavigation/UITransaction.swift
#	Sources/AppKitNavigationShim/include/shim.h
#	Sources/AppKitNavigationShim/shim.m
# Conflicts:
#	Examples/Examples.xcodeproj/project.pbxproj
#	Package.swift
#	Package@swift-6.0.swift
#	Sources/AppKitNavigation/Bindings/NSAlert.swift
#	Sources/AppKitNavigation/Bindings/NSColorPanel.swift
#	Sources/AppKitNavigation/Bindings/NSColorWell.swift
#	Sources/AppKitNavigation/Bindings/NSControl.swift
#	Sources/AppKitNavigation/Bindings/NSDatePicker.swift
#	Sources/AppKitNavigation/Bindings/NSFontManager.swift
#	Sources/AppKitNavigation/Bindings/NSMenuItem.swift
#	Sources/AppKitNavigation/Bindings/NSPathControl.swift
#	Sources/AppKitNavigation/Bindings/NSSaveOpenPanel.swift
#	Sources/AppKitNavigation/Bindings/NSSegmentedControl.swift
#	Sources/AppKitNavigation/Bindings/NSSlider.swift
#	Sources/AppKitNavigation/Bindings/NSStepper.swift
#	Sources/AppKitNavigation/Bindings/NSSwitch.swift
#	Sources/AppKitNavigation/Bindings/NSTextField.swift
#	Sources/AppKitNavigation/SwiftUI/Representable.swift
#	Sources/AppKitNavigationShim/include/shim.h
#	Sources/AppKitNavigationShim/shim.m
#	SwiftNavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved
# Conflicts:
#	Package.swift
#	Package@swift-6.0.swift
#	SwiftNavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved
# Conflicts:
#	Examples/CaseStudies/UIKit/WiFiFeature/WiFiSettingsFeature.swift
#	Package.resolved
#	Package.swift
#	Package@swift-5.9.swift
#	SwiftNavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved
# Conflicts:
#	Package.resolved
# Conflicts:
#	Package.resolved
#	Package.swift
#	Package@swift-5.9.swift
#	Sources/AppKitNavigation/Internal/Exports.swift
#	SwiftNavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved
Adapt AppKitNavigation to the current state of the package after upstream
removed `swift-concurrency-extras`, adopted `InternalImportsByDefault`, and
enabled `ExistentialAny`.

- Replace `import ConcurrencyExtras` with SwiftNavigation's internal
  `LockIsolated` (via `@_spi(Internals) public import SwiftNavigation`);
  switch `withValue`/`@UncheckedSendable` to `withLock`/`nonisolated(unsafe)`
  to match UIKitNavigation's migration.
- Promote `import AppKit` to `public import AppKit` in files that expose
  AppKit types through public API (NSAlert/NSColorPanel/NSColorWell/
  NSDatePicker/NSPathControl/NSProgressIndicator/NSSaveOpenPanel/
  NSSegmentedControl/NSSlider/NSStepper/NSSwitch/NSTextField/Modal/
  Presentation/PresentationContent/Sheet).
- Add `any` to protocol-as-type uses (SheetContent, TargetActionProtocol,
  NSViewControllerPresentationAnimator).
- Drop the redundant `import IssueReporting` in NSSegmentedControl, which
  caused an ambiguous `reportIssue` call under Xcode's stricter resolution.
- Split NSAlert.addButton into two overloads to silence the "default
  expression for inference" warning.
- Clean up Package.swift duplicates (xctest-dynamic-overlay dependency and
  AppKitNavigation target) and propagate AppKitNavigationShim +
  IdentifiedCollections to Package@swift-6.0/6.1 manifests so 6.0/6.1
  toolchains also build.
Introduce `NSView.popover(...)` driven by `NSPopover` (not
`NSViewController.present(_:asPopoverRelativeTo:...)`), mirroring the
existing `PopoverContent` / `PopoverObserver` shape used by Sheet, Modal,
and Presentation.

- `PopoverContent` protocol + default `show`/`close` extension that wires
  `NSPopover.didCloseNotification` into the binding lifecycle so transient
  popovers dismissed by clicking outside reset the source-of-truth.
- Fileprivate `_PopoverPresentation` wraps a user-supplied
  `NSViewController` with an internally-managed `NSPopover` so the public
  `content` closure stays VC-only; popover knobs (`behavior`, `animates`)
  move onto the `popover(...)` method itself.
- `PopoverPresentationsObserver` singleton retains active popovers (an
  `NSPopover` is not self-retained by `show`) and clears them on close,
  mirroring `ModalWindowsObserver`.
- Public API: 5 overloads on `NSView` (`isPresented`, `item: Identifiable`
  value/binding, `item:id:KeyPath` value/binding) with `relativeTo`,
  `preferredEdge`, `behavior`, `animates`, `onDismiss`.
`NSPopover.show(relativeTo:)` was added in macOS 14 to anchor a
popover to a toolbar item. Expose it through the same family of
`popover(...)` bindings already provided on `NSView`, omitting the
`rect` and `preferredEdge` parameters that the toolbar-item API
does not accept.
`Navigated.content` is held weakly and `NSPopover` does not retain its
`PopoverContent` host, so `_PopoverPresentation` was deallocated as soon
as `begin(...)` returned. The `didCloseNotification` sink captured the
host weakly, so `onEndNavigation` ran on a nil reference, the binding
was never reset to `false`, and `navigatedByID` retained the prior id
— blocking any subsequent presentation. Capture `self` strongly in the
close subscription; lifetime stays bounded because the sink removes the
cancellable from `PopoverPresentationsObserver.shared` once the close
notification fires.
# Conflicts:
#	Examples/CaseStudiesTests/NavigationPathTests.swift
#	Package.resolved
Expose `AppKit`, `Combine`, and `IdentifiedCollections` via `public
import` so the types they surface satisfy access-level-on-imports
checks, matching the recently enabled `CheckImplementationOnly`
feature. `Package.resolved` picks up the newly pinned
`swift-collections` and `swift-identified-collections`.
`bind(isAnimated:)` wrapped its observation in a freshly created `ObserveToken`
and returned it without retaining it anywhere. Since the method is marked
`@discardableResult`, the ordinary call site dropped that wrapper, it
deallocated at the end of the statement, and its `deinit` cancelled the
observation behind it. The indicator therefore honoured the binding once,
synchronously, and never reacted to a change again. `init(frame:isAnimated:)`
discards the token internally, so it had the same defect.

Every other binding in the module already stores its token on the receiver via
an associated object; this one was the only exception. Store it the same way and
add `unbindIsAnimated()` to match `unbindURL()` and `unbind(_:)`.

Adds an `AppKitNavigationTests` target, which the module did not have. The
regression test fails against the previous implementation with
`("[false]") is not equal to ("[false, true]")`.
`TargetActionProxy` took over a control's single `target` / `action` pair,
saving the previous one and forwarding to it. That keeps a pair installed
*before* the first binding working, but assigning `target` or `action`
afterwards silently unhooks the proxy, and every binding and closure action on
that control stops firing.

AppKit has stored several target-action pairs per control since macOS 11
(`addTarget:action:forControlEvents:`), which removes the need to seize the pair
at all. The API is only published in the macOS 27 SDK, so declare it in the shim
behind an SDK check that compiles away once that SDK is in use.

The events are not as old as the API, though. Measured on macOS 26.6, a button
click delivers only `trackingBegan` and `trackingEndedInside`; `valueChanged`
and `primaryActionTriggered` are never sent, and tracking events do not stand
for "the action fired" (a keyboard-driven button never tracks). So the proxy
registers as an additional target from macOS 27 on and keeps taking the pair
over below that.

Text fields deliberately do not register `valueChanged`: their per-keystroke
changes already arrive through `textDidChangeNotification` on every version, and
registering both would deliver each edit twice.

Two things to know when touching the shim:

  - Do not copy `NS_SWIFT_NAME(NSControl.Events)` from the SDK. Nesting a type
    inside a class owned by a different module makes the Swift importer drop the
    whole declaration, methods included.
  - Never spell the option set at the call site. It is `NSControlEvents` from the
    shim and `NSControl.Events` from the macOS 27 SDK; an array literal infers
    under both.

Built against both the macOS 26 and macOS 27 SDKs. The macOS 27 path itself is
unverified at runtime for want of a machine running it; the tests are written as
cross-path contracts so running them there confirms it.
Conflicts were confined to the package manifests and the resolved files.

  - Kept the fork's `swift-identified-collections` dependency, which
    AppKitNavigation needs and upstream does not carry, and took upstream's
    versions for everything else.
  - `Package.swift` moved to `swift-issue-reporting` for
    `IssueReportingTestSupport`; the new `AppKitNavigationTests` target follows.
    The 6.0 / 6.1 / 6.2 manifests still use `xctest-dynamic-overlay`, so the
    target is declared with that package there.
  - Upstream added `Package@swift-6.2.swift` carrying its own AppKitNavigation
    target, which only depends on SwiftNavigation. Upstream's AppKitNavigation is
    seven files (animation and binding bridges); the fork's is fifty and also
    needs `AppKitNavigationShim` and `IdentifiedCollections`. Aligned that
    manifest with `Package.swift` and added the shim and test targets, otherwise
    a Swift 6.2 toolchain — which prefers that file — would fail to build.
  - Regenerated `Package.resolved`. The workspace one already pins
    identified-collections transitively through swift-sharing.

Verified: builds under both the Swift 6.2 and 6.4 toolchains, AppKitNavigationTests 8/8.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants