Skip to content

Expose observe overloads for separate tracking and application of changes#286

Open
maximkrouk wants to merge 16 commits intopointfreeco:mainfrom
maximkrouk:expose-observe-function
Open

Expose observe overloads for separate tracking and application of changes#286
maximkrouk wants to merge 16 commits intopointfreeco:mainfrom
maximkrouk:expose-observe-function

Conversation

@maximkrouk
Copy link
Copy Markdown
Contributor

Follow-up PR for #281

The issue:

In code like this:

parentComponent.bind(model)

where

ParentComponent {
  func bind(_ model: ParentModel) {
    observe {
      self.childComponent.bind(model.child)
    }
  }
}

ChildComponent {
  func bind(_ model: ChildModel) {
    observe {
      self.value = model.value
    }
  }
}

call stack will look kinda like this

ParentComponent.bind {
  observe { // #1
    ChildComponent.bind {
      observe { // #2
        // child props
      }
    }
  }
}

And since child props access is nested in observe { // #1, parent will apply self.childComponent.bind(model.child) on any child props change even tho only child should be updated in that case via observe { // #2

Proposed solution

Provide an observe overload that will track and apply changes separately so the pseudocode from above will look like this:

ParentComponent {
  func bind(_ model: ParentModel) {
    observe { _ = model.child } onChange: {
      self.childComponent.bind(model.child)
    }
  }
}

ChildComponent {
  func bind(_ model: ChildModel) {
    observe { // this call can actually stay the same simple observe
      self.value = model.value
    }
  }
}

call stack will look kinda like this

ParentComponent.bind {
  observe() // onChange will be triggered separately from tracking
}

ChildComponent.bind {
  observe { // #2
    // child props
  }
}

Final note

Basically the library adds a cool handling of UITransactions and resubscription to withPerceptionTracking but cuts down the ability to separate tracking and updates which is present in withPerceptionTracking. The solution cannot be replaced with a simple use of withPerceptionTracking since UITransaction-related stuff is library implementation detail, this PR keeps existing functionality, but brings back a lower-level withPerceptionTracking-like API keeping the cool stuff related to UITransaction. The API is not as ergonomic as a basic observe but it handles an important edgecase and it's sufficient for users of the library to build their own APIs based on the new method.

@maximkrouk
Copy link
Copy Markdown
Contributor Author

@stephencelis @mbrandonw Looking forward for your review 🫠

@mbrandonw
Copy link
Copy Markdown
Member

Hi @maximkrouk, I'm sorry but I still don't really understand what you're are trying to achieve here, and why it is any better than just using observe directly. Can you please provide a full, compiling example of something that makes use of these new tools?

Also, have you tried implementing these tools outside of the library? If not, can you try? And if you run into problems can you let us know what those are?

@maximkrouk
Copy link
Copy Markdown
Contributor Author

maximkrouk commented May 20, 2025

Here is a simplified example in an executable swift package maximkrouk/swift-navigation-test-repo:simplified

The issue is that nested observe applictions are triggering parent ones. This behavior is expected because the withPerceptionTracking apply and onChange arguments are combined into a single apply argument in the observe function. However, we must use observe to have UITransaction features enabled, but current observe implementation robs us of the ability to utilize the derived apply and onChange provided separately by withPerceptionTracking.

Redundant updates are present for any amount of nested changes so it'll be a pretty bit problem in a larger application structure like this (pseudocode)

If we set

appView.setModel(appModel)

where

func setModel(_ model:) {
  observe {
    childView.setModel(model.child) // will call observe for child props and probably `setModel` for child.child etc.
  }
}

then

AppView { // ← this
  MainView { // ← this
    Header { // ← this
      Labels { // ← and this update will be triggered
        UsernameLabel() // ← for a simple text change here
      }
    }
  }
}

Implementing smth similar outside the lib requires jumping through hoops, I did smth similar utilizing @testable import as a proof of concept before preparing the PR but this solution it won't work in prod anyway (and it's still pretty bad, you can take a look at the repo, I extracted it to a separate file).

And last but not least I genuinely believe that adding withPerceptionTracking-like API it's just a common sense, not some super niche feature. This helper should be implemented in the library, yes it's a bit less ergonomic, but it does the job and allows to avoid redundant updates and also allows to implement convenience stuff (I will also wrap it and on app-level won't use it directly, however I don't see a good way to handle nested updates without this being merged)

Some examples of what people might do for convenience

// autoclosure-based observation
func observe<Value>(
  _ value: @Sendable @escaping @autoclosure () -> Value,
  onChange: @Sendable @escaping (Value) -> Void
) -> ObserveToken {
  SwiftNavigation.observe { _ = value() } onChange: {
    onChange(value())
  }
}

observe(myModel.text) { label.text = $0 }
// KeyPath-based observation with weak capture
func observeWeak<Object: AnyObject & Perceptible & Sendable, Value>(
  _ object: Object,
  _ keyPath: KeyPath<Object, Value> & Sendable,
  onChange: @Sendable @escaping (Value) -> Void
) -> ObserveToken {
  SwiftNavigation.observe { [weak object] in
    _ = object?[keyPath: keyPath]
  } onChange: { [weak object] in
    object.map { onChange($0[keyPath: keyPath]) }
  }
}

observeWeak(myModel, \.text) { label.text = $0 }

Oh and

why it is any better than just using observe directly

it's not just better, it allows to correctly process nested observables (this is possible with pure withPerceptionTracking btw, but it would kinda break UITransaction-related stuff since it's hidden in observe), which current observe doesn't allow 🌚

@maximkrouk
Copy link
Copy Markdown
Contributor Author

maximkrouk commented May 28, 2025

I was trying to cover the issue extensively, but tldr is:

swift-navigation extends swift-perception with UITransaction but it removes an API for separate tracking and application of changes. It shouldn't be like this at the first place (extending and cutting out stuff on the same level) and this PR only brings back swift-perception-like API extended with UITransaction

Also the example contains external implementation of changes, I see it as hacky, unreliable and not usable in prod.
So it's impossible to implement proposed changes outside of swift-navigation

@stephencelis
Copy link
Copy Markdown
Member

stephencelis commented Sep 23, 2025

@maximkrouk Revisiting this I think we're down to merge this! I merged main into the branch, though, and looks like there are issues due to a refactor of observe that occurred recently for Swift 6.2 support. Think you can take a look soon?

_ tracking: @escaping @MainActor @Sendable () -> Void,
onChange apply: @escaping @MainActor @Sendable () -> Void
) -> ObserveToken {
observe { _ in apply() }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why doesn't this function body ignore the tracking parameter? Could we have a unit test to check that this actually works?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed it has to be used and I fixed it in my local version a while ago, but didn't push it, I'll push an update tomorrow

@maximkrouk
Copy link
Copy Markdown
Contributor Author

maximkrouk commented Sep 23, 2025

@maximkrouk Revisiting this I think we're down to merge this! I merged main into the branch, though, and looks like there are issues due to a refactor of observe that occurred recently for Swift 6.2 support. Think you can take a look soon?

Wow, it's been a while, but I'm glad anyway 😅

I'll take a look and push an update tomorrow

- Fix Xcode26 warnings related to redundant use of @_inheritActorContext
- Fix NSObject.observe
@maximkrouk
Copy link
Copy Markdown
Contributor Author

I left a comment related to this branch here #306 (comment)

@stephencelis
Copy link
Copy Markdown
Member

@maximkrouk Replied, but the short of it is you can ignore those warnings!

@maximkrouk
Copy link
Copy Markdown
Contributor Author

maximkrouk commented Sep 25, 2025

I fixed comments and updated tests but now there are 2 other issues:

  • Task.yeild is not enough to await for updates to take place, had to use Task.sleep in my tests for now
  • Some other tests fail
    • IsolationTests: Assertion fails for both main and global actors
    • LifetimeTests: Relies on Task.yeild

I ran out of time today, so I'll take a look on it tomorrow, but from high level overview of my changes it doesn't look like it should break anything (especially for the old signature with one closure) 🤔

…pose-observe-function

# Conflicts:
#	Sources/SwiftNavigation/Observe.swift
@maximkrouk maximkrouk force-pushed the expose-observe-function branch from 2886926 to 684393d Compare September 25, 2025 17:53
XCTAssertEqual(
MockTracker.shared.entries.withValue(\.count),
13
)
Copy link
Copy Markdown
Contributor Author

@maximkrouk maximkrouk Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order of logs is not consistent here, so I decided to assert by presence of "ParentObject.childUpdate" which is stable across runs and indicates the problem of unscoped observation

@maximkrouk
Copy link
Copy Markdown
Contributor Author

maximkrouk commented Oct 9, 2025

UPD:

  • I found that I missed some @isolated(any) attributes
  • I also tried adding it kinda everywhere and marking task closures as async (didn't help, even tho isolation should've been preserved)
  • ⚠️ Figured out that isolation tests are also failing on the main branch 🫠

Actually ☝️🤓 I thought that ActorProxy was a pretty neat solution and afaik it didn't have warnings (so no unexpected failures should occur on some future swift updates), maybe we should just revert it? 🤔

Task {
await operation()
}
call(operation)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task here was breaking the isolation, maybe marking everything as @_inheritActorContext + @isolated(any) wasn't required, I'll check if it works without it later 🫠

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw Task.yield() is still not sufficient for awaiting for changes to be applied 😔

Copy link
Copy Markdown
Contributor Author

@maximkrouk maximkrouk Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw Task.yield() is still not sufficient for awaiting for changes to be applied 😔
I also noticed that old observe(_:) should use a separate version of onChange/withRecursivePerceptionTracking, current one is calling this shared apply closure too often I'll investigate it a bit later 🫠

Task {
await operation()
}
call(operation)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw Task.yield() is still not sufficient for awaiting for changes to be applied 😔

Task {
await operation()
}
call(operation)
Copy link
Copy Markdown
Contributor Author

@maximkrouk maximkrouk Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw Task.yield() is still not sufficient for awaiting for changes to be applied 😔
I also noticed that old observe(_:) should use a separate version of onChange/withRecursivePerceptionTracking, current one is calling this shared apply closure too often I'll investigate it a bit later 🫠

@Sendable
private func call(_ f: @escaping @Sendable () -> Void) {
f()
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workarounds to make @escaping @isolated(any) @Sendable () -> Void functions callable from sync contexts

TODO: remove @escaping

@stephencelis
Copy link
Copy Markdown
Member

Actually ☝️🤓 I thought that ActorProxy was a pretty neat solution and afaik it didn't have warnings (so no unexpected failures should occur on some future swift updates), maybe we should just revert it? 🤔

It does look like we need to bring it back (#316) but we still want @isolated(any), which is required for some closures to successfully compile in Swift 6.2.

# Conflicts:
#	Sources/SwiftNavigation/Observe.swift
@maximkrouk
Copy link
Copy Markdown
Contributor Author

Tested on Xcode 26.0.1 (17A400) and looks like everything works and in sync with main, even managed to bring back some Task.yeild() instead of sleep()

apply(transaction)
}
} task: { transaction, work in
DispatchQueue.main.async {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably an important change, I used a higher-level API here, to avoid duplication of derived observation handling and removed DispatchQueue.main.async in favor of ActorProxy isolation on explicitly passed MainActor.shared, today I'm too tired to evaluate the impact, maybe I should fallback to duplication, please take a look 🙏

@maximkrouk
Copy link
Copy Markdown
Contributor Author

maximkrouk commented Feb 26, 2026

Added the following overload:

observe(observableObject.property) { propertyValue in
  // handling
}

@autoclosure captures access and gets the property in tracking context, then returned value is handed to onChange handler, this way it's possible to remove not only redundant updates (by decoupling tracking and handling), but also amount of reads for tracking single properties

Note

Didn't test multiple properties, but I'm pretty sure this should work as well

  observe((obj.prop1, obj.prop2)) { tuple in
    print(tuple.0, tuple.1)
  }

But both properties will be read on each change, but it's still an improvement
since they'll be read only once (it's twice on main)

@maximkrouk
Copy link
Copy Markdown
Contributor Author

maximkrouk commented Feb 26, 2026

And an additional simple example for closure-based observe functions introduced in this PR in case you lost context since it's been a while:

  • main branch provides this version + a version with a transaction

    func bindModel(_ model: Model) {
      observe { [weak self] in
        self?.child.bindModel(model.child)
      }
    }
    • child.bindModel will call another observe
    • parent will track child observations since they are setup in parent tracking context, so changes in child.model.value will trigger self?.child.bindModel(model.child)
    • to avoid this we need to separate tracking and handling, so we don't accidentally track smth we didn't mean to track
  • This PR introduces exactly this - the way to separate tracking context from change handlers

    func bindModel(_ model: Model) {
      observe { model.child } onChange: { [weak self] in
        self?.child.bindModel(model.child)
      }
    }
    • we only track model.child changes here as specified
    • child manages it's subscriptions itself

Note

There is still no way to implement it outside of the library without marking a bunch of stuff available with @_spi

@lucamegh
Copy link
Copy Markdown

lucamegh commented Mar 6, 2026

I’m a bit late to the party, but I recently opened a discussion proposing the introduction of an identity-based observation strategy. Is this something you considered when designing this @maximkrouk? With the API introduced in this PR, it's already possible to scope the observed state down to the bare minimum, which is great. However, I think allowing updates to be further constrained so they only trigger when the identity of the observed state changes could make the behavior even safer and more predictable.

Take this example:

class RootViewController: UIViewController {
  let model = Model()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    observe { [weak self] in
      guard let self else {
        return
      }
      
      switch model.destination {
      case .detail(let detailModel):
        print(detailModel)
      }
    }
  }
}

@Observable
class Model {
  var destination: Destination {
    .detail(detailModel)
  }

  @ObservationIgnored
  private lazy var detailModel = DetailModel(string: "test")
  
  enum Destination: Hashable, Identifiable {
    var id: Self { self }
    case detail(DetailModel)
  }
}

@Observable
class DetailModel: HashableObject {
  var string: String
  
  init(string: String) {
    self.string = string
    _ = self.string.count
  }
}

As you can see, self.string is accessed in DetailModel's initializer. At first this may seem harmless, but this will cause the observation in RootViewController to be called every time string changes. This problem would be solved with an observation method like observe<Item, ID: Hashable>(item: @escaping @autoclosure () -> T, id: KeyPath<Item, ID>) { … }.

As you can see, self.string is accessed in DetailModel's initializer. At first glance this may seem harmless, but it causes the observation in RootViewController to be triggered every time DetailModel.string changes.

This could be avoided with a dedicated observe overload that scopes updates based on the identity of the observed item, such as:

observe<Item, ID: Hashable>(item: @escaping @autoclosure () -> Item, id: KeyPath<Item, ID>) { … }

Please refer to my discussion for more details.

cc @stephencelis

@maximkrouk
Copy link
Copy Markdown
Contributor Author

maximkrouk commented Mar 6, 2026

I'm not sure if particualrly

observe<Item, ID: Hashable>(item: @escaping @autoclosure () -> Item, id: KeyPath<Item, ID>) { … }

should be a part of swift-navigation, since id is basically unused afaiu and it's just an opinionated convenience helper (not a bad thing, just not sure if it should be a part of the core lib) that could be implemented outside of the library (when this PR is merged, currently it's kinda impossible due to the absence of scoped observations) but it's great to have a discussion about it. However PR introduces @autoclosure convenience methods, which look generic enough for me, but I just figured that it introduces some room for misuse:

observe(model) { model in
  // this will never be called since users
  // have to observe properties, not just an observable object
}

Maybe smth like

observe<Object: Perceptible, each Value>(
  _ model: Object,
  _ scopes: repeat each (Object) -> Value, // should accept KeyPaths
  onChange: (repeat each Value) -> Void // will provide pure args, without "tupling" them
) -> ObserveToken

will be more suitable for library-level 🤔

present and other higher-level methods that use observe internally could be improved using new observe(_:onChange), probably as a follow-up PR

Note

present(
  item: $model.detail,
  onDismiss: { ... },
  content: { DetailController(model: $0) }
)

Currently if DetailController subscribes to anything in init and not in viewDidLoad() for some reason, any mutation of internally observed stuff will cause present to check the current presentedByID associatedObject since parent observation automatically tracks child ones.

/// observable model in order to populate your view, and also automatically track changes to
/// any fields accessed in the tracking parameter so that the view is always up-to-date.
///
/// - Parameter tracking: A closure that contains properties to track
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// - Parameter tracking: A closure that contains properties to track
/// - Parameter context: A closure that contains properties to track

@discardableResult
public func observe<T>(
_ context: @escaping @MainActor @Sendable @autoclosure () -> T,
onChange apply: @escaping @MainActor @Sendable (_ transaction: UITransaction, T) -> Void
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
onChange apply: @escaping @MainActor @Sendable (_ transaction: UITransaction, T) -> Void
onChange apply: @escaping @MainActor @Sendable (T, _ transaction: UITransaction) -> Void

I think it makes more sense to have the object of the observation as the first argument of the apply closure.

Copy link
Copy Markdown
Contributor Author

@maximkrouk maximkrouk Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't be possible if we introduce variadic generics, but otherwise I think I agree

UPD: I changed the order as proposed since variadic generic won't work with @autoclosure attribute

/// A version of ``observe(_:onChange:)-(()->Void,_)`` that is passed updated value.
///
/// - Parameter tracking: A closure that contains properties to track
/// - Parameter onChange: Invoked when the value of a property changes
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// - Parameter onChange: Invoked when the value of a property changes
/// - Parameter apply: Invoked when the value of a property changes

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though, in general, I would promote the label onChange to be the parameter name and get rid of apply. That's more similar to withObservationTracking

@lucamegh
Copy link
Copy Markdown

lucamegh commented Mar 6, 2026

I still think this flavor of observation would be worth adding to the core library. swift-navigation already provides both generic and more specialized tools. For example, both present(item:) and navigationDestination(item:) are built on top of the far more generic destination(item:content:present:dismiss:).

I see the current version of observe as the generic primitive, while both your proposal and mine would act as more specialized conveniences built on top of it.

Regarding @autoclosure, as mentioned here, it can make the API quite easy to misuse from a memory-management perspective.

@maximkrouk
Copy link
Copy Markdown
Contributor Author

For example, both present(item:) and navigationDestination(item:) are built on top of the far more generic destination(item:content:present:dismiss:).

But navigation helpers use all the parameters, id is needed for tracking presented controllers 💁‍♂️

And observe semantically just recursively observes anything Observable that is accessed in tracking context, any additional mappings can be built on top of the core functions

For example my old local API looked kinda like this:

updateBindings([ // array of Cancellable
  model.observe(\.count.description, onChange: assign(to: \.countLabel.text)),
  ...
])

where Model conforms to UIComponentModel protocol that enables custom observe function that accepts KeyPath<Self, Value> and UIView conforms to UIComponent protocol that enables stuff like assign(to: KeyPath<Self, T>)->(T)->Void

And I think such stuff should be local.

Note

Btw I had to conform ObserveToken to Cancellable retroactively, does it make sense to introduce this conformance in swift-navigation with conditional Combine import? @stephencelis

It can make the API quite easy to misuse from a memory-management perspective

Not sure about that, I mentioned ergonomics perspective, users would be able to pass an Observable object with an intention of observing any changes, but since there is no access to properties, nothing is observed, so
observe(_:Object, _ context: (Object) -> Property, onChange: (Property) -> Void) -> ObserveToken
can be superior in this context, looking forward for Stephen's opinion c:

But from the memory management perspective users still get a closure-based version to capture objects weakly explicitly, in general strong capture is likely to be users' default for the tracking contexts and weak capture for onChange blocks

@maximkrouk
Copy link
Copy Markdown
Contributor Author

maximkrouk commented Mar 6, 2026

Oh, I just figured that we're basically talking about the same thing :D
But argument labels confused me
observe(item: model, id: \.property) { id in } vs observe(model, \.property) { value in }

It's basically the same but:

  • There is no need for Hashable conformance for id
  • I find item label a bit redundant on the call site
  • I find id label a bit misleading since thinking about "observing value" is probably an easier (and more straightforward) way of thinking rather than "observing object, but observation is identified by the changes of the property"

This method is

  • ✅ less prone to misuse than @autoclosure
  • ⚠️ doesn't allow for batch-observation of multiple properties
    But it should be fine to simply use multiple scoped observe calls or use explicit closure for tracking multiple properties

One more thing to thing about is base function tracking context signature, currently it's () -> Void, but maybe it'll be nice to use () -> T where T is discarded automatically

// MARK: - Current

// Pros: 
//   - no unused types/params
// Cons:
//   - users have to discard result for single properties manually
//   - users have to discard result for multiple properties manually
// Note:
//   - single props can be tracked with `observe(model, \.property) { print($0 }`
observe { _ = model.property } onChange: { print(model.value) }

// MARK: - Proposed

// Pros: 
//   - users don't have to discard result for single properties manually
// Cons:
//   - value returned from tracking context is implicitly discarded
//   - users have to discard result for multiple properties manually
// Note:
//   - single props can be tracked with `observe(model, \.property) { print($0 }`
observe { model.property } onChange: { print(model.value) }

Options:

  • Keep everything as is
  • Introduce discarded result
  • Always pass result to onChange, introduce overload for Void

@lucamegh
Copy link
Copy Markdown

lucamegh commented Mar 6, 2026

observe(model, \.property) { value in } makes sense as a default. Optionally, though, I would still add the ability not to receive updates when the identity of the value hasn't changed: observe(model, \.property, id: \.id) { value in }.

Again taking inspiration from the two type two types of navigation functions:

// No id filtering (similar to what you propose)

@discardableResult
    public func destination<Item>(
      item: UIBinding<Item?>,
      content: @escaping (UIBinding<Item>) -> UIViewController,
      present: @escaping (UIViewController, UITransaction) -> Void,
      dismiss:
        @escaping (
          _ child: UIViewController,
          _ transaction: UITransaction
        ) -> Void
    ) -> ObserveToken {
      destination(
        item: item,
        id: { _ in nil },
        content: content,
        present: present,
        dismiss: dismiss
      )
    }

// Id filtering

@discardableResult
    public func destination<Item, ID: Hashable>(
      item: UIBinding<Item?>,
      id: KeyPath<Item, ID>,
      content: @escaping (UIBinding<Item>) -> UIViewController,
      present:
        @escaping (
          _ child: UIViewController,
          _ transaction: UITransaction
        ) -> Void,
      dismiss:
        @escaping (
          _ child: UIViewController,
          _ transaction: UITransaction
        ) -> Void
    ) -> ObserveToken {
      destination(
        item: item,
        id: { $0[keyPath: id] },
        content: content,
        present: present,
        dismiss: dismiss
      )
    }

Given how high-level and generic this tool is, I think identity-based observation is worth including in the core library. Maybe I could just PR it after this on gets merged.

@maximkrouk
Copy link
Copy Markdown
Contributor Author

Well, observe(_:_:id:onChange:) makes sense, but it's still fairly easy to implement outside of the lib, and as you see even observation scoping takes months to merge, and it's a feature that requires depending on fork :D

I don't think it's necessary, and you could just make a swift-navigation-id-observation package for such purposes (it would also derive support responsibility which is quite nice given that pointfree already supports a ton of amazing packages) 🤔

But maybe it's just me and the feature is more useful than I perceive it 💁‍♂️

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.

5 participants