Skip to content

Commit 0343f86

Browse files
committed
rxswift-core-architecture
Signed-off-by: Mike Packard <nguyenphong.mobile.engineer@gmail.com>
0 parents  commit 0343f86

File tree

25 files changed

+3037
-0
lines changed

25 files changed

+3037
-0
lines changed

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Bucket
3+
uuid = "C3C26736-93C2-42C2-8EBC-0079D914396C"
4+
type = "1"
5+
version = "2.0">
6+
<Breakpoints>
7+
<BreakpointProxy
8+
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
9+
<BreakpointContent
10+
uuid = "71A65BCD-43D5-423F-8056-7C94DA60174D"
11+
shouldBeEnabled = "Yes"
12+
ignoreCount = "0"
13+
continueAfterRunningActions = "No"
14+
filePath = "Sources/ComposableArchitecture/Store.swift"
15+
startingColumnNumber = "9223372036854775807"
16+
endingColumnNumber = "9223372036854775807"
17+
startingLineNumber = "404"
18+
endingLineNumber = "404"
19+
landmarkName = "stateless"
20+
landmarkType = "24">
21+
</BreakpointContent>
22+
</BreakpointProxy>
23+
</Breakpoints>
24+
</Bucket>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>SchemeUserState</key>
6+
<dict>
7+
<key>Rx (Playground) 1.xcscheme</key>
8+
<dict>
9+
<key>isShown</key>
10+
<false/>
11+
<key>orderHint</key>
12+
<integer>5</integer>
13+
</dict>
14+
<key>Rx (Playground) 2.xcscheme</key>
15+
<dict>
16+
<key>isShown</key>
17+
<false/>
18+
<key>orderHint</key>
19+
<integer>6</integer>
20+
</dict>
21+
<key>Rx (Playground).xcscheme</key>
22+
<dict>
23+
<key>isShown</key>
24+
<false/>
25+
<key>orderHint</key>
26+
<integer>4</integer>
27+
</dict>
28+
<key>rxswift-composable-architecture.xcscheme_^#shared#^_</key>
29+
<dict>
30+
<key>orderHint</key>
31+
<integer>0</integer>
32+
</dict>
33+
</dict>
34+
<key>SuppressBuildableAutocreation</key>
35+
<dict>
36+
<key>ComposableArchitecture</key>
37+
<dict>
38+
<key>primary</key>
39+
<true/>
40+
</dict>
41+
</dict>
42+
</dict>
43+
</plist>

Package.resolved

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// swift-tools-version:5.5
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "rxswift-composable-architecture",
8+
platforms: [
9+
.iOS(.v11),
10+
.macOS(.v10_14),
11+
.tvOS(.v12),
12+
.watchOS(.v5),
13+
],
14+
products: [
15+
.library(
16+
name: "ComposableArchitecture",
17+
targets: ["ComposableArchitecture"]),
18+
],
19+
dependencies: [
20+
.package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.2.0"),
21+
.package(url: "https://github.com/pointfreeco/swift-case-paths", from: "0.7.0"),
22+
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.2.1"),
23+
],
24+
targets: [
25+
.target(
26+
name: "ComposableArchitecture",
27+
dependencies: [
28+
"RxSwift",
29+
.product(name: "RxRelay", package: "RxSwift"),
30+
.product(name: "CasePaths", package: "swift-case-paths"),
31+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
32+
]),
33+
]
34+
)

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# rxswift-core-architecture
2+
3+
## Example
4+
5+
https://github.com/FullStack-Swift/TodoFullStackSwift
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import CasePaths
2+
import Dispatch
3+
4+
/// Determines how the string description of an action should be printed when using the
5+
/// ``Reducer/debug(_:state:action:actionFormat:environment:)`` higher-order reducer.
6+
public enum ActionFormat {
7+
/// Prints the action in a single line by only specifying the labels of the associated values:
8+
///
9+
/// ```swift
10+
/// Action.screenA(.row(index:, action: .textChanged(query:)))
11+
/// ```
12+
///
13+
case labelsOnly
14+
/// Prints the action in a multiline, pretty-printed format, including all the labels of
15+
/// any associated values, as well as the data held in the associated values:
16+
///
17+
/// ```swift
18+
/// Action.screenA(
19+
/// ScreenA.row(
20+
/// index: 1,
21+
/// action: RowAction.textChanged(
22+
/// query: "Hi"
23+
/// )
24+
/// )
25+
/// )
26+
/// ```
27+
///
28+
case prettyPrint
29+
}
30+
31+
extension Reducer {
32+
/// Prints debug messages describing all received actions and state mutations.
33+
///
34+
/// Printing is only done in debug (`#if DEBUG`) builds.
35+
///
36+
/// - Parameters:
37+
/// - prefix: A string with which to prefix all debug messages.
38+
/// - toDebugEnvironment: A function that transforms an environment into a debug environment by
39+
/// describing a print function and a queue to print from. Defaults to a function that ignores
40+
/// the environment and returns a default ``DebugEnvironment`` that uses Swift's `print`
41+
/// function and a background queue.
42+
/// - Returns: A reducer that prints debug messages for all received actions.
43+
public func debug(
44+
_ prefix: String = "",
45+
actionFormat: ActionFormat = .prettyPrint,
46+
environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in
47+
DebugEnvironment()
48+
}
49+
) -> Reducer {
50+
self.debug(
51+
prefix,
52+
state: { $0 },
53+
action: .self,
54+
actionFormat: actionFormat,
55+
environment: toDebugEnvironment
56+
)
57+
}
58+
59+
/// Prints debug messages describing all received actions.
60+
///
61+
/// Printing is only done in debug (`#if DEBUG`) builds.
62+
///
63+
/// - Parameters:
64+
/// - prefix: A string with which to prefix all debug messages.
65+
/// - toDebugEnvironment: A function that transforms an environment into a debug environment by
66+
/// describing a print function and a queue to print from. Defaults to a function that ignores
67+
/// the environment and returns a default ``DebugEnvironment`` that uses Swift's `print`
68+
/// function and a background queue.
69+
/// - Returns: A reducer that prints debug messages for all received actions.
70+
public func debugActions(
71+
_ prefix: String = "",
72+
actionFormat: ActionFormat = .prettyPrint,
73+
environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in
74+
DebugEnvironment()
75+
}
76+
) -> Reducer {
77+
self.debug(
78+
prefix,
79+
state: { _ in () },
80+
action: .self,
81+
actionFormat: actionFormat,
82+
environment: toDebugEnvironment
83+
)
84+
}
85+
86+
/// Prints debug messages describing all received local actions and local state mutations.
87+
///
88+
/// Printing is only done in debug (`#if DEBUG`) builds.
89+
///
90+
/// - Parameters:
91+
/// - prefix: A string with which to prefix all debug messages.
92+
/// - toLocalState: A function that filters state to be printed.
93+
/// - toLocalAction: A case path that filters actions that are printed.
94+
/// - toDebugEnvironment: A function that transforms an environment into a debug environment by
95+
/// describing a print function and a queue to print from. Defaults to a function that ignores
96+
/// the environment and returns a default ``DebugEnvironment`` that uses Swift's `print`
97+
/// function and a background queue.
98+
/// - Returns: A reducer that prints debug messages for all received actions.
99+
public func debug<LocalState, LocalAction>(
100+
_ prefix: String = "",
101+
state toLocalState: @escaping (State) -> LocalState,
102+
action toLocalAction: CasePath<Action, LocalAction>,
103+
actionFormat: ActionFormat = .prettyPrint,
104+
environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in
105+
DebugEnvironment()
106+
}
107+
) -> Reducer {
108+
return self
109+
}
110+
}
111+
112+
/// An environment for debug-printing reducers.
113+
public struct DebugEnvironment {
114+
public var printer: (String) -> Void
115+
public var queue: DispatchQueue
116+
117+
public init(
118+
printer: @escaping (String) -> Void = { print($0) },
119+
queue: DispatchQueue
120+
) {
121+
self.printer = printer
122+
self.queue = queue
123+
}
124+
125+
public init(
126+
printer: @escaping (String) -> Void = { print($0) }
127+
) {
128+
self.init(printer: printer, queue: _queue)
129+
}
130+
}
131+
132+
private let _queue = DispatchQueue(
133+
label: "co.pointfree.ComposableArchitecture.DebugEnvironment",
134+
qos: .background
135+
)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import Combine
2+
import os.signpost
3+
4+
@available(iOS 12.0, *)
5+
extension Reducer {
6+
/// Instruments the reducer with
7+
/// [signposts](https://developer.apple.com/documentation/os/logging/recording_performance_data).
8+
/// Each invocation of the reducer will be measured by an interval, and the lifecycle of its
9+
/// effects will be measured with interval and event signposts.
10+
///
11+
/// To use, build your app for Instruments (⌘I), create a blank instrument, and then use the "+"
12+
/// icon at top right to add the signpost instrument. Start recording your app (red button at top
13+
/// left) and then you should see timing information for every action sent to the store and every
14+
/// effect executed.
15+
///
16+
/// Effect instrumentation can be particularly useful for inspecting the lifecycle of long-living
17+
/// effects. For example, if you start an effect (e.g. a location manager) in `onAppear` and
18+
/// forget to tear down the effect in `onDisappear`, it will clearly show in Instruments that the
19+
/// effect never completed.
20+
///
21+
/// - Parameters:
22+
/// - prefix: A string to print at the beginning of the formatted message for the signpost.
23+
/// - log: An `OSLog` to use for signposts.
24+
/// - Returns: A reducer that has been enhanced with instrumentation.
25+
public func signpost(
26+
_ prefix: String = "",
27+
log: OSLog = OSLog(
28+
subsystem: "co.pointfree.composable-architecture",
29+
category: "Reducer Instrumentation"
30+
)
31+
) -> Self {
32+
guard log.signpostsEnabled else { return self }
33+
34+
// NB: Prevent rendering as "N/A" in Instruments
35+
let zeroWidthSpace = "\u{200B}"
36+
37+
let prefix = prefix.isEmpty ? zeroWidthSpace : "[\(prefix)] "
38+
39+
return Self { state, action, environment in
40+
var actionOutput: String!
41+
if log.signpostsEnabled {
42+
actionOutput = debugCaseOutput(action)
43+
os_signpost(.begin, log: log, name: "Action", "%s%s", prefix, actionOutput)
44+
}
45+
let effects = self.run(&state, action, environment)
46+
if log.signpostsEnabled {
47+
os_signpost(.end, log: log, name: "Action")
48+
return effects
49+
.effectSignpost(prefix, log: log, actionOutput: actionOutput)
50+
.eraseToEffect()
51+
}
52+
return effects
53+
}
54+
}
55+
}
56+
57+
@available(iOS 12.0, *)
58+
extension Effect {
59+
func effectSignpost(
60+
_ prefix: String,
61+
log: OSLog,
62+
actionOutput: String
63+
) -> Self {
64+
let sid = OSSignpostID(log: log)
65+
return self.asObservable().do(afterNext: { value in
66+
os_signpost(
67+
.event, log: log, name: "Effect Output", "%sOutput from %s", prefix, actionOutput)
68+
}, afterError: { _ in
69+
os_signpost(.end, log: log, name: "Effect", signpostID: sid, "%sCancelled", prefix)
70+
}, afterCompleted: {
71+
os_signpost(.end, log: log, name: "Effect", signpostID: sid, "%sFinished", prefix)
72+
}, onSubscribed: {
73+
os_signpost(
74+
.begin, log: log, name: "Effect", signpostID: sid, "%sStarted from %s", prefix,
75+
actionOutput)
76+
}, onDispose: {
77+
os_signpost(.end, log: log, name: "Effect", signpostID: sid, "%sCancelled", prefix)
78+
})
79+
.eraseToEffect()
80+
}
81+
}
82+
83+
func debugCaseOutput(_ value: Any) -> String {
84+
func debugCaseOutputHelp(_ value: Any) -> String {
85+
let mirror = Mirror(reflecting: value)
86+
switch mirror.displayStyle {
87+
case .enum:
88+
guard let child = mirror.children.first else {
89+
let childOutput = "\(value)"
90+
return childOutput == "\(type(of: value))" ? "" : ".\(childOutput)"
91+
}
92+
let childOutput = debugCaseOutputHelp(child.value)
93+
return ".\(child.label ?? "")\(childOutput.isEmpty ? "" : "(\(childOutput))")"
94+
case .tuple:
95+
return mirror.children.map { label, value in
96+
let childOutput = debugCaseOutputHelp(value)
97+
return "\(label.map { isUnlabeledArgument($0) ? "_:" : "\($0):" } ?? "")\(childOutput.isEmpty ? "" : " \(childOutput)")"
98+
}
99+
.joined(separator: ", ")
100+
default:
101+
return ""
102+
}
103+
}
104+
105+
return "\(type(of: value))\(debugCaseOutputHelp(value))"
106+
}
107+
108+
private func isUnlabeledArgument(_ label: String) -> Bool {
109+
label.firstIndex(where: { $0 != "." && !$0.isNumber }) == nil
110+
}

0 commit comments

Comments
 (0)