Skip to content

Commit

Permalink
Clean up (#187)
Browse files Browse the repository at this point in the history
* Clean up

Some trailing fixes after merging #167.

* wip

* wip

* fix todo

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* renames

---------

Co-authored-by: Brandon Williams <[email protected]>
  • Loading branch information
stephencelis and mbrandonw authored Aug 2, 2024
1 parent ef8a522 commit 181925d
Show file tree
Hide file tree
Showing 27 changed files with 296 additions and 84 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ body:
required: false
- label: If possible, I've reproduced the issue using the `main` branch of this package.
required: false
- label: This issue hasn't been addressed in an [existing GitHub issue](https://github.com/pointfreeco/swiftui-navigation/issues) or [discussion](https://github.com/pointfreeco/swiftui-navigation/discussions).
- label: This issue hasn't been addressed in an [existing GitHub issue](https://github.com/pointfreeco/swift-navigation/issues) or [discussion](https://github.com/pointfreeco/swiftui-navigation/discussions).
required: true
- type: textarea
attributes:
Expand Down
6 changes: 3 additions & 3 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ blank_issues_enabled: false

contact_links:
- name: Project Discussion
url: https://github.com/pointfreeco/swiftui-navigation/discussions
url: https://github.com/pointfreeco/swift-navigation/discussions
about: SwiftUI Navigation Q&A, ideas, and more
- name: Documentation
url: https://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/
url: https://pointfreeco.github.io/swift-navigation/main/documentation/swiftnavigation/
about: Read SwiftUI Navigation's documentation
- name: Videos
url: https://www.pointfree.co/collections/swiftui-navigation
url: https://www.pointfree.co/
about: Watch videos to get a behind-the-scenes look at how SwiftUI Navigation was motivated and built
- name: Slack
url: https://www.pointfree.co/slack-invite
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ on:
jobs:
swift_format:
name: swift-format
runs-on: macOS-13
runs-on: macOS-14
steps:
- uses: actions/checkout@v4
- name: Xcode Select
run: sudo xcode-select -s /Applications/Xcode_15.0.app
run: sudo xcode-select -s /Applications/Xcode_15.4.app
- name: Install
run: brew install swift-format
- name: Format
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ jobs:
env:
INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_PROJECT_CHANNEL_WEBHOOK_URL }}
with:
text: swiftui-navigation ${{ github.event.release.tag_name }} has been released.
text: swift-navigation ${{ github.event.release.tag_name }} has been released.
blocks: |
[
{
"type": "header",
"text": {
"type": "plain_text",
"text": "swiftui-navigation ${{ github.event.release.tag_name}}"
"text": "swift-navigation ${{ github.event.release.tag_name}}"
}
},
{
Expand Down Expand Up @@ -56,14 +56,14 @@ jobs:
env:
INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
with:
text: swiftui-navigation ${{ github.event.release.tag_name }} has been released.
text: swift-navigation ${{ github.event.release.tag_name }} has been released.
blocks: |
[
{
"type": "header",
"text": {
"type": "plain_text",
"text": "swiftui-navigation ${{ github.event.release.tag_name}}"
"text": "swift-navigation ${{ github.event.release.tag_name}}"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "aa764c0971d6509b93ddc56d1e092436ef9ac6683f7bc8c469eb1d95229d0ce9",
"originHash" : "808eb8e1d07661e8b57a786176eca2c0701a6e550ecfe221b0585e8b058c8ebd",
"pins" : [
{
"identity" : "combine-schedulers",
Expand Down Expand Up @@ -40,7 +40,7 @@
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras.git",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71",
"version" : "1.1.0"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ The tools provided by this library can also form the foundation of building navi
non-Apple platforms, such as Windows, Linux, Wasm and more. We do not currently provide any such
tools at this moment, but it is possible for them to be built externally.

For example, in Wasm it is possible to use the ``observe(_:isolation:)`` function to observe changes
For example, in Wasm it is possible to use the ``observe(isolation:_:)-93yzu`` function to observe changes
to a model and update the DOM:

```swift
Expand All @@ -247,7 +247,7 @@ import JavaScriptKit
var countLabel = document.createElement("span")
_ = document.body.appendChild(countLabel)

let token = observe { _ in
let token = observe {
countLabel.innerText = .string("Count: \(model.count)")
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ The tools provided by this library can also form the foundation of building navi
non-Apple platforms, such as Windows, Linux, Wasm and more. We do not currently provide any such
tools at this moment, but it is possible for them to be built externally.

For example, in Wasm it is possible to use the ``observe(_:isolation:)`` function to observe changes
to a model and update the DOM:
For example, in Wasm it is possible to use the ``observe(isolation:_:)-93yzu`` function to observe
changes to a model and update the DOM:

```swift
import JavaScriptKit

var countLabel = document.createElement("span")
_ = document.body.appendChild(countLabel)

let token = observe { _ in
let token = observe {
countLabel.innerText = .string("Count: \(model.count)")
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ Driving navigation from state like this can be incredibly powerful:
This is why state-driven navigation is so great, and SwiftUI does a pretty great job at providing
these tools. However, there are ways to improve SwiftUI's tools, _and_ its possible to bring
state-driven tools to other Apple frameworks such as UIKit and AppKit, and even to other non-Apple
_platforms_, such as Windows, Linux, Wasm and more.
_platforms_, such as Windows, Linux, Wasm, and more.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# ``SwiftNavigation/observe(isolation:_:)-93yzu``

## Topics

### Attaching data to observation

- ``observe(isolation:_:)-5rirm``
4 changes: 2 additions & 2 deletions Sources/SwiftNavigation/Documentation.docc/SwiftNavigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ for non-Apple platforms, such as Windows, Linux, Wasm, and more.
The SwiftNavigation library forms the foundation that more advanced tools can be built upon, such
as the UIKitNavigation and SwiftUINavigation libraries. There are two primary tools provided:

* ``observe(_:isolation:)``: Minimally observe changes in a model.
* ``observe(isolation:_:)-93yzu``: Minimally observe changes in a model.
* ``UIBinding``: Two-way binding for connecting navigation and UI components to an observable model.

In addition to these tools there are some supplementary concepts that allow you to build more
Expand All @@ -36,7 +36,7 @@ SwiftUI, UIKit, AppKit, and even non-Apple platforms.

### Observing changes to state

- ``observe(_:isolation:)``
- ``observe(isolation:_:)-93yzu``
- ``ObservationToken``

### Creating and sharing state
Expand Down
15 changes: 15 additions & 0 deletions Sources/SwiftNavigation/Internal/HashableStaticString.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package struct HashableStaticString: Hashable {
package var rawValue: StaticString

package init(rawValue: StaticString) {
self.rawValue = rawValue
}

package static func == (lhs: Self, rhs: Self) -> Bool {
lhs.rawValue.description == rhs.rawValue.description
}

package func hash(into hasher: inout Hasher) {
hasher.combine(rawValue.description)
}
}
30 changes: 23 additions & 7 deletions Sources/SwiftNavigation/Observe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import ConcurrencyExtras
/// > Observation framework.
///
/// This function also works on non-Apple platforms, such as Windows, Linux, Wasm, and more. For
/// example, in a Wasm app you could observe chnages to the `count` property to update the inner
/// example, in a Wasm app you could observe changes to the `count` property to update the inner
/// HTML of a tag:
///
/// ```swift
Expand All @@ -43,21 +43,37 @@ import ConcurrencyExtras
/// var countLabel = document.createElement("span")
/// _ = document.body.appendChild(countLabel)
///
/// let token = observe { _ in
/// let token = observe {
/// countLabel.innerText = .string("Count: \(model.count)")
/// }
/// ```
///
/// And you can also build your own tools on top of ``observe(_:isolation:)``.
/// And you can also build your own tools on top of `observe`.
///
/// - Parameters:
/// - isolation: The isolation of the observation.
/// - apply: A closure that contains properties to track.
/// - Returns: A token that keeps the subscription alive. Observation is cancelled when the token
/// is deallocated.
public func observe(
isolation: (any Actor)? = #isolation,
@_inheritActorContext _ apply: @escaping @Sendable () -> Void
) -> ObservationToken {
observe(isolation: isolation) { _ in apply() }
}

/// Tracks access to properties of an observable model.
///
/// A version of ``observe(isolation:_:)`` that is handed the current ``UITransaction``.
///
/// - Parameters:
/// - isolation: The isolation of the observation.
/// - apply: A closure that contains properties to track.
/// - Returns: A token that keeps the subscription alive. Observation is cancelled when the token
/// is deallocated.
/// is deallocated.
public func observe(
@_inheritActorContext _ apply: @escaping @Sendable (_ transaction: UITransaction) -> Void,
isolation: (any Actor)? = #isolation
isolation: (any Actor)? = #isolation,
@_inheritActorContext _ apply: @escaping @Sendable (_ transaction: UITransaction) -> Void
) -> ObservationToken {
let actor = ActorProxy(base: isolation)
return observe(
Expand Down Expand Up @@ -159,7 +175,7 @@ public final class ObservationToken: @unchecked Sendable, HashableObject {
cancel()
}

/// Cancels observation that was created with ``observe(_:isolation:)``.
/// Cancels observation that was created with ``observe(isolation:_:)-93yzu``.
///
/// > Note: This cancellation is lazy and cooperative. It does not cancel the observation
/// > immediately, but rather next time a change is detected by `observe` it will cease any future
Expand Down
96 changes: 76 additions & 20 deletions Sources/SwiftNavigation/UIBinding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,13 @@ import IssueReporting
/// > `Perceptible` protocols, use the [`@UIBindable`](<doc:UIBindable>) property wrapper.
///
/// It is also possible to use bindings for UI components on other platforms beyond Apple's
/// platforms. For example, in Wasm
/// platforms. For example, in Wasm you can bind an HTML text field to a field of the model:
///
/// ```swift
/// @UIBindable var model = Model()
///
/// let textField = // TODO
/// let searchField = document.createElement("input")
/// searchField.bind(text: $model.searchText)
/// ```
///
/// This makes it so that any changes to the text field in the DOM are immediately played back
Expand Down Expand Up @@ -115,14 +116,16 @@ import IssueReporting
/// ```
///
/// And you can also build your own navigation tools by utilizing ``UIBinding`` and
/// ``observe(_:isolation:)``. You can even build navigation tools for non-Apple platforms, such as
/// Windows, Linux, Wasm and more.
/// ``observe(isolation:_:)-93yzu``. You can even build navigation tools for non-Apple platforms,
/// such as Windows, Linux, Wasm and more.
///
/// For example, it is possible to build a tool that drives alerts in HTML from a binding of a
/// boolean:
///
/// ```swift
/// alert(isPresented: $model.isPresented) // TODO
/// alert(isPresented: $model.isErrorAlertPresented) {
/// "Something went wrong."
/// }
/// ```
@dynamicMemberLookup
@propertyWrapper
Expand Down Expand Up @@ -241,8 +244,23 @@ public struct UIBinding<Value>: Sendable {
/// Creates a binding by projecting the base optional value to a Boolean value.
///
/// - Parameter base: A value to project to a Boolean value.
public init<V>(_ base: UIBinding<V?>) where Value == Bool {
self = base.isPresented
public init<V>(
_ base: UIBinding<V?>,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) where Value == Bool {
func open(_ location: some _UIBinding<V?>) -> any _UIBinding<Value> {
_UIBindingOptionalToBool(
base: location,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}
self.init(location: open(base.location), transaction: base.transaction)
}

/// Creates a binding by projecting the base value to an optional value.
Expand Down Expand Up @@ -665,6 +683,57 @@ where Base.Value: CasePathable {
}
}

private final class _UIBindingOptionalToBool<
Base: _UIBinding<Wrapped?>, Wrapped
>: _UIBinding, @unchecked Sendable {
let base: Base
let fileID: StaticString
let filePath: StaticString
let line: UInt
let column: UInt
init(
base: Base,
fileID: StaticString,
filePath: StaticString,
line: UInt,
column: UInt
) {
self.base = base
self.fileID = fileID
self.filePath = filePath
self.line = line
self.column = column
}
var wrappedValue: Bool {
get { base.wrappedValue != nil }
set {
if newValue {
reportIssue(
"""
Boolean presentation binding attempted to write 'true' to a generic 'UIBinding<Item?>' \
(i.e., 'UIBinding<\(Wrapped.self)?>').
This is not a valid thing to do, as there is no way to convert 'true' to a new instance \
of '\(Wrapped.self)'.
""",
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
} else {
base.wrappedValue = nil
}
}
}
static func == (lhs: _UIBindingOptionalToBool, rhs: _UIBindingOptionalToBool) -> Bool {
lhs.base == rhs.base
}
func hash(into hasher: inout Hasher) {
hasher.combine(base)
}
}

private final class _UIBindingOptionalToMember<
Base: _UIBinding<Wrapped?>, Wrapped, Value
>: _UIBinding, @unchecked Sendable {
Expand Down Expand Up @@ -724,16 +793,3 @@ private final class _UIBindingOptionalEnumToCase<
hasher.combine(keyPath)
}
}

extension Optional {
fileprivate var isPresented: Bool {
get { self != nil }
set {
guard !newValue else {
// TODO: runtimeWarn?
return
}
self = nil
}
}
}
4 changes: 3 additions & 1 deletion Sources/UIKitNavigation/Bindings/UIControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@
}
}
let observationToken = ObservationToken { [weak self] in
MainActor.assumeIsolated { self?.removeAction(action, for: .allEvents) }
MainActor.assumeIsolated {
self?.removeAction(action, for: .allEvents)
}
token.cancel()
observation.invalidate()
}
Expand Down
Loading

0 comments on commit 181925d

Please sign in to comment.