Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement StateObject property wrapper #260

Merged
merged 4 commits into from
Aug 17, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import CombineShim
class MountedCompositeElement<R: Renderer>: MountedElement<R> {
let parentTarget: R.TargetType

/** An array that stores type-erased values captured with the `@State` property wrappers used
in declarations of this element.
/** An array that stores type-erased values captured with the `@State` and `@StateObject` property
wrappers used in declarations of this element.
*/
var state = [Any]()
var storage = [Any]()

/** An array that stores subscriptions to updates on `@ObservableObject` property wrappers used
in declarations of this element. These subscriptions are transient and may be cleaned up on
Expand Down
32 changes: 19 additions & 13 deletions Sources/TokamakCore/StackReconciler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,12 @@ public final class StackReconciler<R: Renderer> {
}
}

private func queueStateUpdate(
private func queueStorageUpdate(
for mountedElement: MountedCompositeElement<R>,
id: Int,
updater: (inout Any) -> ()
) {
updater(&mountedElement.state[id])
updater(&mountedElement.storage[id])
queueUpdate(for: mountedElement)
}

Expand All @@ -125,7 +125,7 @@ public final class StackReconciler<R: Renderer> {
queuedRerenders.removeAll()
}

private func setupState(
private func setupStorage(
id: Int,
for property: PropertyInfo,
of compositeElement: MountedCompositeElement<R>,
Expand All @@ -134,23 +134,28 @@ public final class StackReconciler<R: Renderer> {
// swiftlint:disable force_try
// `ValueStorage` property already filtered out, so safe to assume the value's type
// swiftlint:disable:next force_cast
var state = try! property.get(from: compositeElement[keyPath: bodyKeypath]) as! ValueStorage
var storage = try! property.get(from: compositeElement[keyPath: bodyKeypath]) as! ValueStorage

if compositeElement.state.count == id {
compositeElement.state.append(state.anyInitialValue)
if compositeElement.storage.count == id {
compositeElement.storage.append(storage.anyInitialValue)
}

if state.getter == nil || state.setter == nil {
state.getter = { compositeElement.state[id] }
if storage.getter == nil {
storage.getter = { compositeElement.storage[id] }

guard var writableStorage = storage as? WritableValueStorage else {
return try! property.set(value: storage, on: &compositeElement[keyPath: bodyKeypath])
}

// Avoiding an indirect reference cycle here: this closure can be owned by callbacks
// owned by view's target, which is strongly referenced by the reconciler.
state.setter = { [weak self, weak compositeElement] newValue in
writableStorage.setter = { [weak self, weak compositeElement] newValue in
guard let element = compositeElement else { return }
self?.queueStateUpdate(for: element, id: id) { $0 = newValue }
self?.queueStorageUpdate(for: element, id: id) { $0 = newValue }
}

try! property.set(value: writableStorage, on: &compositeElement[keyPath: bodyKeypath])
}
try! property.set(value: state, on: &compositeElement[keyPath: bodyKeypath])
}

private func setupTransientSubscription(
Expand Down Expand Up @@ -207,9 +212,10 @@ public final class StackReconciler<R: Renderer> {
for property in dynamicProps {
// Setup state/subscriptions
if property.type is ValueStorage.Type {
setupState(id: stateIdx, for: property, of: compositeElement, body: bodyKeypath)
setupStorage(id: stateIdx, for: property, of: compositeElement, body: bodyKeypath)
stateIdx += 1
} else if property.type is ObservedProperty.Type {
}
if property.type is ObservedProperty.Type {
setupTransientSubscription(for: property, of: compositeElement, body: bodyKeypath)
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/TokamakCore/State/ObservedObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ public struct ObservedObject<ObjectType>: DynamicProperty where ObjectType: Obse
}

public let projectedValue: Wrapper
}

extension ObservedObject: ObservedProperty {
var objectWillChange: AnyPublisher<(), Never> {
wrappedValue.objectWillChange.map { _ in }.eraseToAnyPublisher()
}
}

extension ObservedObject: ObservedProperty {}
7 changes: 5 additions & 2 deletions Sources/TokamakCore/State/State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
//
protocol ValueStorage {
var getter: (() -> Any)? { get set }
var setter: ((Any) -> ())? { get set }
var anyInitialValue: Any { get }
}

protocol WritableValueStorage: ValueStorage {
var setter: ((Any) -> ())? { get set }
}

@propertyWrapper public struct State<Value>: DynamicProperty {
private let initialValue: Value

Expand All @@ -46,7 +49,7 @@ protocol ValueStorage {
}
}

extension State: ValueStorage {}
extension State: WritableValueStorage {}

extension State where Value: ExpressibleByNilLiteral {
@inlinable
Expand Down
30 changes: 29 additions & 1 deletion Sources/TokamakCore/State/StateObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,32 @@
// See the License for the specific language governing permissions and
// limitations under the License.

public typealias StateObject = ObservedObject
import CombineShim

@propertyWrapper
public struct StateObject<ObjectType: ObservableObject>: DynamicProperty {
public var wrappedValue: ObjectType { (getter?() as? ObservedObject.Wrapper)?.root ?? initial() }

let initial: () -> ObjectType
var getter: (() -> Any)?

public init(wrappedValue initial: @autoclosure @escaping () -> ObjectType) {
self.initial = initial
}

public var projectedValue: ObservedObject<ObjectType>.Wrapper {
getter?() as? ObservedObject.Wrapper ?? ObservedObject.Wrapper(root: initial())
}
}

extension StateObject: ObservedProperty {
var objectWillChange: AnyPublisher<(), Never> {
wrappedValue.objectWillChange.map { _ in }.eraseToAnyPublisher()
}
}

extension StateObject: ValueStorage {
var anyInitialValue: Any {
ObservedObject.Wrapper(root: initial())
}
}
2 changes: 1 addition & 1 deletion Sources/TokamakCore/Views/Navigation/NavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class NavigationContext: ObservableObject {
public struct NavigationView<Content>: View where Content: View {
let content: Content

@ObservedObject var context = NavigationContext()
@StateObject var context = NavigationContext()

public init(@ViewBuilder content: () -> Content) {
self.content = content()
Expand Down
1 change: 1 addition & 0 deletions Sources/TokamakDOM/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public typealias ObservableObject = TokamakCore.ObservableObject
public typealias ObservedObject = TokamakCore.ObservedObject
public typealias Published = TokamakCore.Published
public typealias State = TokamakCore.State
public typealias StateObject = TokamakCore.StateObject

// MARK: Modifiers & Styles

Expand Down