Skip to content

Commit

Permalink
WithViewStore debug state diffs (#706)
Browse files Browse the repository at this point in the history
* WithViewStore debug state diffs

* More debug checks

* fix

* wip
  • Loading branch information
stephencelis authored Aug 11, 2021
1 parent 2d1e21b commit e73b36e
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 116 deletions.
221 changes: 136 additions & 85 deletions Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,79 @@ import SwiftUI
/// [here](https://gist.github.com/mbrandonw/dee2ceac2c316a1619cfdf1dc7945f66)).
public struct WithViewStore<State, Action, Content> {
private let content: (ViewStore<State, Action>) -> Content
private var prefix: String?
#if DEBUG
private let file: StaticString
private let line: UInt
private var prefix: String?
private var previousState: (State) -> State?
#endif
@ObservedObject private var viewStore: ViewStore<State, Action>

fileprivate init(
store: Store<State, Action>,
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
file: StaticString = #fileID,
line: UInt = #line,
content: @escaping (ViewStore<State, Action>) -> Content
) {
self.content = content
#if DEBUG
self.file = file
self.line = line
var previousState: State? = nil
self.previousState = { currentState in
defer { previousState = currentState }
return previousState
}
#endif
self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
}

/// Prints debug information to the console whenever the view is computed.
///
/// - Parameter prefix: A string with which to prefix all debug messages.
/// - Returns: A structure that prints debug messages for all computations.
public func debug(_ prefix: String = "") -> Self {
var view = self
view.prefix = prefix
#if DEBUG
view.prefix = prefix
#endif
return view
}

fileprivate var _body: Content {
#if DEBUG
if let prefix = self.prefix {
let difference = self.previousState(self.viewStore.state)
.map {
debugDiff($0, self.viewStore.state).map { "(Changed state)\n\($0)" }
?? "(No difference in state detected)"
}
?? "(Initial state)\n\(debugOutput(self.viewStore.state, indent: 2))"
func typeName(_ type: Any.Type) -> String {
var name = String(reflecting: type)
if let index = name.firstIndex(of: ".") {
name.removeSubrange(...index)
}
return name
}
print(
"""
\(prefix.isEmpty ? "" : "\(prefix): ")\
WithViewStore<\(typeName(State.self)), \(typeName(Action.self)), _>\
@\(self.file):\(self.line) \(difference)
"""
)
}
#endif
return self.content(self.viewStore)
}
}

extension WithViewStore: View where Content: View {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute views from store state.

///
/// - Parameters:
/// - store: A store.
/// - isDuplicate: A function to determine when two `State` values are equal. When values are
Expand All @@ -45,28 +100,25 @@ extension WithViewStore: View where Content: View {
public init(
_ store: Store<State, Action>,
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
file: StaticString = #fileID,
line: UInt = #line,
@ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.content = content
self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
self.init(
store: store,
removeDuplicates: isDuplicate,
file: file,
line: line,
content: content
)
}

public var body: Content {
#if DEBUG
if let prefix = self.prefix {
print(
"""
\(prefix.isEmpty ? "" : "\(prefix): ")\
Evaluating WithViewStore<\(State.self), \(Action.self), ...>.body
"""
)
}
#endif
return self.content(self.viewStore)
self._body
}
}

extension WithViewStore where Content: View, State: Equatable {
extension WithViewStore where State: Equatable, Content: View {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute views from equatable store state.
///
Expand All @@ -75,13 +127,15 @@ extension WithViewStore where Content: View, State: Equatable {
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
file: StaticString = #fileID,
line: UInt = #line,
@ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.init(store, removeDuplicates: ==, content: content)
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
}
}

extension WithViewStore where Content: View, State == Void {
extension WithViewStore where State == Void, Content: View {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute views from equatable store state.
///
Expand All @@ -90,9 +144,11 @@ extension WithViewStore where Content: View, State == Void {
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
file: StaticString = #fileID,
line: UInt = #line,
@ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.init(store, removeDuplicates: ==, content: content)
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
}
}

Expand All @@ -104,74 +160,69 @@ extension WithViewStore: DynamicViewContent where State: Collection, Content: Dy
}
}

#if compiler(>=5.3)
import SwiftUI

/// A structure that transforms a store into an observable view store in order to compute scenes
/// from store state.
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
extension WithViewStore: Scene where Content: Scene {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute scenes from store state.

/// - Parameters:
/// - store: A store.
/// - isDuplicate: A function to determine when two `State` values are equal. When values are
/// equal, repeat view computations are removed,
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.content = content
self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
}
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
extension WithViewStore: Scene where Content: Scene {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute views from store state.
///
/// - Parameters:
/// - store: A store.
/// - isDuplicate: A function to determine when two `State` values are equal. When values are
/// equal, repeat view computations are removed,
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
file: StaticString = #fileID,
line: UInt = #line,
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.init(
store: store,
removeDuplicates: isDuplicate,
file: file,
line: line,
content: content
)
}

public var body: Content {
#if DEBUG
if let prefix = self.prefix {
print(
"""
\(prefix.isEmpty ? "" : "\(prefix): ")\
Evaluating WithViewStore<\(State.self), \(Action.self), ...>.body
"""
)
}
#endif
return self.content(self.viewStore)
}
public var body: Content {
self._body
}
}

@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
extension WithViewStore where Content: Scene, State: Equatable {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute views from equatable store state.
///
/// - Parameters:
/// - store: A store of equatable state.
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.init(store, removeDuplicates: ==, content: content)
}
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
extension WithViewStore where State: Equatable, Content: Scene {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute scenes from equatable store state.
///
/// - Parameters:
/// - store: A store of equatable state.
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
file: StaticString = #fileID,
line: UInt = #line,
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
}
}

@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
extension WithViewStore where Content: Scene, State == Void {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute views from equatable store state.
///
/// - Parameters:
/// - store: A store of equatable state.
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.init(store, removeDuplicates: ==, content: content)
}
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
extension WithViewStore where State == Void, Content: Scene {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute scenes from equatable store state.
///
/// - Parameters:
/// - store: A store of equatable state.
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
file: StaticString = #fileID,
line: UInt = #line,
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
}
#endif
}
60 changes: 29 additions & 31 deletions Tests/ComposableArchitectureTests/WithViewStoreAppTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,36 @@
import ComposableArchitecture
import SwiftUI

#if compiler(>=5.3)
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
struct TestApp: App {
let store = Store(
initialState: 0,
reducer: Reducer<Int, Void, Void> { state, _, _ in
state += 1
return .none
},
environment: ()
)

var body: some Scene {
WithViewStore(self.store) { viewStore in
#if os(iOS) || os(macOS)
WindowGroup {
EmptyView()
}
.commands {
CommandMenu("Commands") {
Button("Increment") {
viewStore.send(())
}
.keyboardShortcut("+")
}
}
#else
WindowGroup {
EmptyView()
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
struct TestApp: App {
let store = Store(
initialState: 0,
reducer: Reducer<Int, Void, Void> { state, _, _ in
state += 1
return .none
},
environment: ()
)

var body: some Scene {
WithViewStore(self.store) { viewStore in
#if os(iOS) || os(macOS)
WindowGroup {
EmptyView()
}
.commands {
CommandMenu("Commands") {
Button("Increment") {
viewStore.send(())
}
#endif
.keyboardShortcut("+")
}
}
#else
WindowGroup {
EmptyView()
}
#endif
}
}
#endif
}

0 comments on commit e73b36e

Please sign in to comment.