Skip to content

Commit

Permalink
AtomScope no longer inherits overrides (#104)
Browse files Browse the repository at this point in the history
* AtomScope no longer inherits overrides from its ancestor scopes

* Update README

* Update comments and error messages
  • Loading branch information
ra1028 authored Apr 15, 2024
1 parent 6ff9d3e commit 9d49fc7
Show file tree
Hide file tree
Showing 14 changed files with 343 additions and 217 deletions.
37 changes: 20 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1221,26 +1221,29 @@ struct NewsView: View {

#### Override

Values and states defined by atoms can be overridden in root or in any scope.
If you override an atom in [AtomRoot](#atomroot), it will override the values throughout the app, which is useful for dependency injection. In case you want to override an atom only in a limited scope, you might like to use [AtomScope](#atomscope) instead to override as it substitutes the atom value only in that scope, which can be useful for injecting dependencies that are needed only for the scope or overriding state in certain views.
Overriding an atom in [AtomRoot](#atomroot) or [AtomScope](#atomscope) overwrites its value when used in the descendant views, which is useful for dependency injection or swapping state in a particular view.

```swift
AtomRoot {
VStack {
CountStepper()

AtomScope {
CountDisplay()
}
.override(CounterAtom()) { _ in
// Overrides the count to be 456 only for the display content.
456
}
}
AtomScope {
CountDisplay()
}
.override(CounterAtom()) { _ in
// Overrides the count to be 123 throughout the app.
123
456 // Overrides the count to be `456` only for this scope.
}
```

Note that when multiple `AtomScope`s are nested, it doesn't inherit the overrides of its ancestor scopes.
In this case, you can explicitly inherit overrides from the parent scope by passing a `@ViewContext` context that has gotten in the parent scope.

```swift
@ViewContext
var context

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

Expand Down Expand Up @@ -1592,7 +1595,7 @@ struct RootView: View {
Text("Example View")
}
.sheet(isPresented: $isPresented) {
AtomScope(context) {
AtomScope(inheriting: context) {
MailView()
}
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/Atoms/AtomRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ public struct AtomRoot<Content: View>: View {
public var body: some View {
content.environment(
\.store,
.scoped(
key: ScopeKey(token: state.token),
store: state.store,
StoreContext(
state.store,
scopeKey: ScopeKey(token: state.token),
observers: observers,
overrides: overrides
)
Expand Down
126 changes: 102 additions & 24 deletions Sources/Atoms/AtomScope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import SwiftUI
///
/// var body: some View {
/// MyUIViewWrappingView {
/// AtomScope(context) {
/// AtomScope(inheriting: context) {
/// MySwiftUIView()
/// }
/// }
Expand All @@ -37,11 +37,11 @@ import SwiftUI
///
/// Additionally, if for some reason your app cannot use ``AtomRoot`` to manage the store,
/// you can instead manage the store on your own and pass the instance to ``AtomScope``
/// to allow descendant views to use atoms.
/// to allow descendant views to store atom values in the given store.
///
/// ```swift
/// let store = AtomStore()
/// let rootView = AtomScope(store) {
/// let rootView = AtomScope(storesIn: store) {
/// RootView()
/// }
/// let window = UIWindow(frame: UIScreen.main.bounds)
Expand All @@ -50,21 +50,16 @@ import SwiftUI
/// ```
///
public struct AtomScope<Content: View>: View {
@StateObject
private var state = State()
private let store: StoreContext?
private let inheritance: Inheritance
private var overrides = [OverrideKey: any AtomOverrideProtocol]()
private var observers = [Observer]()
private let content: Content

@Environment(\.store)
private var environmentStore

/// Creates a new scope with the specified content.
///
/// - Parameter content: The view content that inheriting from the parent.
public init(@ViewBuilder content: () -> Content) {
self.store = nil
self.inheritance = .environment
self.content = content()
}

Expand All @@ -75,10 +70,10 @@ public struct AtomScope<Content: View>: View {
/// - context: The parent view context that for inheriting store explicitly.
/// - content: The view content that inheriting from the parent.
public init(
_ context: AtomViewContext,
inheriting context: AtomViewContext,
@ViewBuilder content: () -> Content
) {
self.store = context._store
self.inheritance = .context(context)
self.content = content()
}

Expand All @@ -89,23 +84,39 @@ public struct AtomScope<Content: View>: View {
/// - store: An object that stores the state of atoms.
/// - content: The view content that inheriting from the parent.
public init(
_ store: AtomStore,
storesIn store: AtomStore,
@ViewBuilder content: () -> Content
) {
self.store = StoreContext(store)
self.inheritance = .store(store)
self.content = content()
}

/// The content and behavior of the view.
public var body: some View {
content.environment(
\.store,
(store ?? environmentStore).scoped(
key: ScopeKey(token: state.token),
observers: observers,
overrides: overrides
switch inheritance {
case .context(let context):
InheritedContext(
content: content,
context: context,
overrides: overrides,
observers: observers
)

case .store(let store):
InheritedStore(
content: content,
store: store,
overrides: overrides,
observers: observers
)
)

case .environment:
InheritedEnvironment(
content: content,
overrides: overrides,
observers: observers
)
}
}

/// For debugging purposes, each time there is a change in the internal state,
Expand All @@ -127,7 +138,7 @@ public struct AtomScope<Content: View>: View {
/// When accessing the overridden atom, this context will create and return the given value
/// instead of the atom value.
///
/// Note that unlike override by ``AtomRoot``, this will only override atoms used in this scope.
/// This only overrides atoms used in this scope and never be inherited to a nested scope.
///
/// - Parameters:
/// - atom: An atom to be overridden.
Expand All @@ -145,7 +156,7 @@ public struct AtomScope<Content: View>: View {
/// When accessing the overridden atom, this context will create and return the given value
/// instead of the atom value.
///
/// Note that unlike override by ``AtomRoot``, this will only override atoms used in this scope.
/// This only overrides atoms used in this scope and never be inherited to a nested scope.
///
/// - Parameters:
/// - atomType: An atom type to be overridden.
Expand All @@ -158,8 +169,75 @@ public struct AtomScope<Content: View>: View {
}

private extension AtomScope {
enum Inheritance {
case context(AtomViewContext)
case store(AtomStore)
case environment
}

struct InheritedContext: View {
let content: Content
let context: AtomViewContext
let overrides: [OverrideKey: any AtomOverrideProtocol]
let observers: [Observer]

var body: some View {
content.environment(
\.store,
context._store.inherited(
observers: observers,
overrides: overrides
)
)
}
}

struct InheritedStore: View {
let content: Content
let store: AtomStore
let overrides: [OverrideKey: any AtomOverrideProtocol]
let observers: [Observer]

@StateObject
private var state = ScopeState()

var body: some View {
content.environment(
\.store,
StoreContext(
store,
scopeKey: ScopeKey(token: state.token),
observers: observers,
overrides: overrides
)
)
}
}

struct InheritedEnvironment: View {
let content: Content
let overrides: [OverrideKey: any AtomOverrideProtocol]
let observers: [Observer]

@StateObject
private var state = ScopeState()
@Environment(\.store)
private var environmentStore

var body: some View {
content.environment(
\.store,
environmentStore.scoped(
scopeKey: ScopeKey(token: state.token),
observers: observers,
overrides: overrides
)
)
}
}

@MainActor
final class State: ObservableObject {
final class ScopeState: ObservableObject {
let token = ScopeKey.Token()
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/Atoms/Context/AtomTestContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -439,9 +439,9 @@ internal extension AtomTestContext {

@usableFromInline
var _store: StoreContext {
.scoped(
key: ScopeKey(token: _state.token),
store: _state.store,
StoreContext(
_state.store,
scopeKey: ScopeKey(token: _state.token),
observers: [],
overrides: _state.overrides
)
Expand Down
30 changes: 16 additions & 14 deletions Sources/Atoms/Core/AtomKey.swift
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
internal struct AtomKey: Hashable {
private let key: AnyHashable
private let type: ObjectIdentifier
private let overrideScopeKey: ScopeKey?
private let getName: () -> String

var isOverridden: Bool {
overrideScopeKey != nil
}
private let scopeKey: ScopeKey?
private let anyAtomType: Any.Type

var debugLabel: String {
if let overrideScopeKey {
return getName() + "-override:\(overrideScopeKey.debugLabel)"
let atomLabel = String(describing: anyAtomType)

if let scopeKey {
return atomLabel + "-scoped:\(scopeKey.debugLabel)"
}
else {
return getName()
return atomLabel
}
}

init<Node: Atom>(_ atom: Node, overrideScopeKey: ScopeKey?) {
var isScoped: Bool {
scopeKey != nil
}

init<Node: Atom>(_ atom: Node, scopeKey: ScopeKey?) {
self.key = AnyHashable(atom.key)
self.type = ObjectIdentifier(Node.self)
self.overrideScopeKey = overrideScopeKey
self.getName = { String(describing: Node.self) }
self.scopeKey = scopeKey
self.anyAtomType = Node.self
}

func hash(into hasher: inout Hasher) {
hasher.combine(key)
hasher.combine(type)
hasher.combine(overrideScopeKey)
hasher.combine(scopeKey)
}

static func == (lhs: Self, rhs: Self) -> Bool {
lhs.key == rhs.key && lhs.type == rhs.type && lhs.overrideScopeKey == rhs.overrideScopeKey
lhs.key == rhs.key && lhs.type == rhs.type && lhs.scopeKey == rhs.scopeKey
}
}
23 changes: 0 additions & 23 deletions Sources/Atoms/Core/AtomOverride.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ internal protocol AtomOverrideProtocol {
associatedtype Node: Atom

var value: (Node) -> Node.Loader.Value { get }

func scoped(key: ScopeKey) -> any AtomScopedOverrideProtocol
}

@usableFromInline
Expand All @@ -16,25 +14,4 @@ internal struct AtomOverride<Node: Atom>: AtomOverrideProtocol {
init(value: @escaping (Node) -> Node.Loader.Value) {
self.value = value
}

@usableFromInline
func scoped(key: ScopeKey) -> any AtomScopedOverrideProtocol {
AtomScopedOverride<Node>(scopeKey: key, value: value)
}
}

// As a workaround to the problem of not getting ScopeKey synchronously
// when the AtomRoot or AtomScope's override modifier is called, those modifiers
// temporarily register AtomOverride and convert them to AtomScopedOverride when
// their View body is evaluated. This is not ideal from a performance standpoint,
// so it will be improved as soon as an alternative way to grant per-scope keys
// independent of the SwiftUI lifecycle is came up.
@usableFromInline
internal protocol AtomScopedOverrideProtocol {
var scopeKey: ScopeKey { get }
}

internal struct AtomScopedOverride<Node: Atom>: AtomScopedOverrideProtocol {
let scopeKey: ScopeKey
let value: (Node) -> Node.Loader.Value
}
8 changes: 7 additions & 1 deletion Sources/Atoms/Core/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ internal extension EnvironmentValues {

private struct StoreEnvironmentKey: EnvironmentKey {
static var defaultValue: StoreContext {
StoreContext(enablesAssertion: true)
StoreContext(
nil,
scopeKey: ScopeKey(token: ScopeKey.Token()),
observers: [],
overrides: [:],
enablesAssertion: true
)
}
}
Loading

0 comments on commit 9d49fc7

Please sign in to comment.