Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 19 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,23 @@ AtomScope {
}
```

#### [AtomDerivedScope](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomderivedscope)

`AtomDerivedScope` is useful when, for some reason, the atom store is not propagated through a view hierarchy. It explicitly propagate the atom store from the parent view via the given view context.

```swift
struct MailView: View {
@ViewContext
var context

var body: some View {
AtomDerivedScope(context) {
WrappedMailView()
}
}
}
```

#### [Suspense](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/suspense)

`Suspense` awaits the resulting value of the given `Task` and displays the content depending on its phase.
Expand Down Expand Up @@ -1476,20 +1493,6 @@ AtomScope {
}
```

If you want to inherit the overridden atom from the parent scope, you can explicitly pass `@ViewContext` context that has gotten in the parent scope. Then, the new scope completely inherits the parent scope's context.

```swift
@ViewContext
var context

var body: some {
// Inherites the parent scope's overrides.
AtomScope(inheriting: context) {
CountDisplay()
}
}
```

Note that overridden atoms in `AtomScope` automatically be scoped, but other atoms that depend on them will be in a shared state and must be given `Scoped` attribute (See also: [Scoped Atom](#scoped-atom)) in order to avoid it from being shared across out of scope.

See [Testing](#testing) section for details on dependency injection on unit tests.
Expand Down Expand Up @@ -1803,7 +1806,7 @@ class MessageLoader: ObservableObject {
#### Modal presentation causes assertionFailure when dismissing it (Fixed in iOS15)

Unfortunately, SwiftUI has a bug in iOS14 or lower where the `EnvironmentValue` is removed from a screen presented with `.sheet` just before dismissing it. Since this library is designed based on `EnvironmentValue`, this bug end up triggering the friendly `assertionFailure` that is added so that developers can easily aware of forgotten `AtomRoot` implementation.
As a workaround, `AtomScope` has the ability to explicitly inherit the store through `AtomViewContext` from the parent view.
As a workaround, you can use `AtomDerivedScope` to explicitly propagate the atom store via `AtomViewContext` from the parent view.

<details><summary><code>💡 Click to expand workaround</code></summary>

Expand All @@ -1820,7 +1823,7 @@ struct RootView: View {
Text("Example View")
}
.sheet(isPresented: $isPresented) {
AtomScope(inheriting: context) {
AtomDerivedScope(context) {
MailView()
}
}
Expand Down
48 changes: 48 additions & 0 deletions Sources/Atoms/AtomDerivedScope.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import SwiftUI

/// A view that derives the parent context.
///
/// Sometimes SwiftUI fails to propagate environment values in the view tree for some reason.
/// This is a critical problem because the centralized state store of atoms is propagated through
/// a view hierarchy via environment values.
/// The typical example is that, in case you use SwiftUI view inside UIKit view, it could fail as
/// SwiftUI can't pass environment values to UIKit across boundaries.
/// In that case, you can wrap the view with ``AtomDerivedScope`` and pass a view context to it so that
/// the descendant views can explicitly propagate the atom store.
///
/// ```swift
/// @ViewContext
/// var context
///
/// var body: some View {
/// MyUIViewWrappingView {
/// AtomDerivedScope(context) {
/// MySwiftUIView()
/// }
/// }
/// }
/// ```
///
public struct AtomDerivedScope<Content: View>: View {
private let store: StoreContext
private let content: Content

/// Creates a derived scope with the specified content that will be allowed to use atoms by
/// passing a view context to explicitly make the descendant views propagate the atom store.
///
/// - Parameters:
/// - context: The parent view context that provides the atom store.
/// - content: The descendant view content.
public init(
_ context: AtomViewContext,
@ViewBuilder content: () -> Content
) {
self.store = context._store
self.content = content()
}

/// The content and behavior of the view.
public var body: some View {
content.environment(\.store, store)
}
}
44 changes: 7 additions & 37 deletions Sources/Atoms/AtomScope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,6 @@ import SwiftUI
/// }
/// ```
///
/// It inherits from the atom store provided by ``AtomRoot`` through environment values by default,
/// but sometimes SwiftUI can fail to pass environment values in the view-tree for some reason.
/// The typical example is that, in case you use SwiftUI view inside UIKit view, it could fail as
/// SwiftUI can't pass environment values to UIKit across boundaries.
/// In that case, you can wrap the view with ``AtomScope`` and pass a view context to it so that
/// the descendant views can explicitly inherit the store.
///
/// ```swift
/// @ViewContext
/// var context
///
/// var body: some View {
/// MyUIViewWrappingView {
/// AtomScope(inheriting: context) {
/// MySwiftUIView()
/// }
/// }
/// }
/// ```
///
public struct AtomScope<Content: View>: View {
private let inheritance: Inheritance
private var observers = [Observer]()
Expand All @@ -70,12 +50,12 @@ public struct AtomScope<Content: View>: View {
/// - Parameters:
/// - context: The parent view context that for inheriting store explicitly.
/// - content: The descendant view content that provides scoped context for atoms.
@available(*, deprecated, message: "Use `AtomDerivedScope` instead")
public init(
inheriting context: AtomViewContext,
@ViewBuilder content: () -> Content
) {
let store = context._store
self.inheritance = .context(store: store)
self.inheritance = .context(context)
self.content = content()
}

Expand All @@ -90,11 +70,10 @@ public struct AtomScope<Content: View>: View {
content: content
)

case .context(let store):
WithContext(
store: store,
content: content
)
case .context(let context):
AtomDerivedScope(context) {
content
}
}
}

Expand Down Expand Up @@ -173,7 +152,7 @@ public struct AtomScope<Content: View>: View {
private extension AtomScope {
enum Inheritance {
case environment(scopeID: ScopeID)
case context(store: StoreContext)
case context(AtomViewContext)
}

struct WithEnvironment: View {
Expand All @@ -199,13 +178,4 @@ private extension AtomScope {
)
}
}

struct WithContext: View {
let store: StoreContext
let content: Content

var body: some View {
content.environment(\.store, store)
}
}
}
1 change: 1 addition & 0 deletions Sources/Atoms/Atoms.docc/Atoms.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Building state by compositing atoms automatically optimizes rendering based on i

- ``AtomRoot``
- ``AtomScope``
- ``AtomDerivedScope``
- ``Suspense``

### Values
Expand Down
12 changes: 6 additions & 6 deletions Sources/Atoms/PropertyWrapper/ViewContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ private extension ViewContext {
}
```

If for some reason the view tree is formed that does not inherit from `EnvironmentValues`,
consider using `AtomScope` to pass it.
If for some reason the view tree does not propagate `EnvironmentValues`,
consider using `AtomDerivedScope` to propagate it explicitly.
That happens when using SwiftUI view wrapped with `UIHostingController`.

```
Expand All @@ -104,21 +104,21 @@ private extension ViewContext {

var body: some View {
UIViewWrappingView {
AtomScope(inheriting: context) {
AtomDerivedScope(context) {
WrappedView()
}
}
}
}
```

The modal screen presented by the `.sheet` modifier or etc, inherits from the environment values,
The modal screen presented by the `.sheet` modifier or etc, propagates the environment values,
but only in iOS14, there is a bug where the environment values will be dismantled during it is
dismissing. This also can be avoided by using `AtomScope` to explicitly inherit from it.
dismissing. This also can be avoided by using `AtomDerivedScope` to explicitly propagate it.

```
.sheet(isPresented: ...) {
AtomScope(inheriting: context) {
AtomDerivedScope(context) {
ExampleView()
}
}
Expand Down