├── .gitignore
├── App
├── Resources
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── App_Store_1024pt_1x.png
│ │ │ ├── Contents.json
│ │ │ ├── iPhone_App_60pt_2x.png
│ │ │ ├── iPhone_App_60pt_3x.png
│ │ │ ├── iPhone_Notification_20pt_2x.png
│ │ │ ├── iPhone_Notification_20pt_3x.png
│ │ │ ├── iPhone_Settings_29pt_2x.png
│ │ │ ├── iPhone_Settings_29pt_3x.png
│ │ │ ├── iPhone_Spotlight_40pt_2x.png
│ │ │ └── iPhone_Spotlight_40pt_3x.png
│ │ └── Contents.json
│ ├── Info.plist
│ └── InfoTests.plist
├── Sources
│ └── App.swift
└── Tests
│ └── AppTests.swift
├── Color
├── .gitignore
├── .swiftpm
│ └── xcode
│ │ └── xcshareddata
│ │ └── xcschemes
│ │ └── Color.xcscheme
├── Package.swift
├── Sources
│ ├── ColorAction.swift
│ ├── ColorReducer.swift
│ ├── ColorState.swift
│ ├── ColorView.swift
│ └── RGBColor.swift
└── Tests
│ ├── ColorTests.swift
│ └── __Snapshots__
│ └── ColorTests
│ ├── testBlackColorSnapshot.dark.png
│ ├── testBlackColorSnapshot.light.png
│ ├── testPreviewSnapshot.dark.png
│ ├── testPreviewSnapshot.light.png
│ ├── testWhiteColorSnapshot.dark.png
│ └── testWhiteColorSnapshot.light.png
├── Common
├── .gitignore
├── .swiftpm
│ └── xcode
│ │ └── xcshareddata
│ │ └── xcschemes
│ │ └── Common.xcscheme
├── Package.swift
├── Sources
│ └── IsRunningTests.swift
└── Tests
│ └── IsRunningTestsTests.swift
├── ComposableApp.xcodeproj
├── project.pbxproj
└── xcshareddata
│ └── xcschemes
│ ├── ComposableApp.xcscheme
│ └── Tests.xcscheme
├── LICENSE
├── Misc
├── color_screen.png
├── preview_screen.png
└── shape_screen.png
├── Preview
├── .gitignore
├── .swiftpm
│ └── xcode
│ │ └── xcshareddata
│ │ └── xcschemes
│ │ └── Preview.xcscheme
├── Package.swift
├── Sources
│ ├── PreviewAction.swift
│ ├── PreviewReducer.swift
│ ├── PreviewState.swift
│ └── PreviewView.swift
└── Tests
│ ├── PreviewTests.swift
│ └── __Snapshots__
│ └── PreviewTests
│ ├── testCircleSnapshot.dark.png
│ ├── testCircleSnapshot.light.png
│ ├── testEmptySnapshot.dark.png
│ ├── testEmptySnapshot.light.png
│ ├── testPreviewSnapshot.dark.png
│ ├── testPreviewSnapshot.light.png
│ ├── testSquareSnapshot.dark.png
│ └── testSquareSnapshot.light.png
├── README.md
├── Shape
├── .gitignore
├── .swiftpm
│ └── xcode
│ │ └── xcshareddata
│ │ └── xcschemes
│ │ └── Shape.xcscheme
├── Package.swift
├── Sources
│ ├── ShapeAction.swift
│ ├── ShapeReducer.swift
│ ├── ShapeState.swift
│ ├── ShapeType.swift
│ └── ShapeView.swift
└── Tests
│ ├── ShapeTests.swift
│ └── __Snapshots__
│ └── ShapeTests
│ ├── testCircleSnapshot.dark.png
│ ├── testCircleSnapshot.light.png
│ ├── testPreviewSnapshot.dark.png
│ ├── testPreviewSnapshot.light.png
│ ├── testSquareSnapshot.dark.png
│ └── testSquareSnapshot.light.png
├── Tabs
├── .gitignore
├── .swiftpm
│ └── xcode
│ │ └── xcshareddata
│ │ └── xcschemes
│ │ └── Tabs.xcscheme
├── Package.swift
├── Sources
│ ├── Tab.swift
│ ├── TabsAction.swift
│ ├── TabsReducer.swift
│ ├── TabsState.swift
│ └── TabsView.swift
└── Tests
│ ├── TabsTests.swift
│ └── __Snapshots__
│ └── TabsTests
│ ├── testColorTabSnapshot.dark.png
│ ├── testColorTabSnapshot.light.png
│ ├── testPreviewSnapshot.dark.png
│ ├── testPreviewSnapshot.light.png
│ ├── testPreviewTabSnapshot.dark.png
│ ├── testPreviewTabSnapshot.light.png
│ ├── testShapeTabSnapshot.dark.png
│ └── testShapeTabSnapshot.light.png
└── Testing
├── .gitignore
├── .swiftpm
└── xcode
│ └── xcshareddata
│ └── xcschemes
│ └── Testing.xcscheme
├── Package.swift
└── Sources
└── SnapshotTesting.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # macOS
2 | *.DS_Store
3 |
4 | # Xcode
5 | project.xcworkspace/
6 | xcuserdata/
7 |
--------------------------------------------------------------------------------
/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/App/Resources/Assets.xcassets/AppIcon.appiconset/App_Store_1024pt_1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/App/Resources/Assets.xcassets/AppIcon.appiconset/App_Store_1024pt_1x.png
--------------------------------------------------------------------------------
/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "iPhone_Notification_20pt_2x.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "iPhone_Notification_20pt_3x.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "iPhone_Settings_29pt_2x.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "iPhone_Settings_29pt_3x.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "iPhone_Spotlight_40pt_2x.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "iPhone_Spotlight_40pt_3x.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "iPhone_App_60pt_2x.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "iPhone_App_60pt_3x.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "App_Store_1024pt_1x.png",
53 | "idiom" : "ios-marketing",
54 | "scale" : "1x",
55 | "size" : "1024x1024"
56 | }
57 | ],
58 | "info" : {
59 | "author" : "xcode",
60 | "version" : 1
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_App_60pt_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_App_60pt_2x.png
--------------------------------------------------------------------------------
/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_App_60pt_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_App_60pt_3x.png
--------------------------------------------------------------------------------
/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Notification_20pt_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Notification_20pt_2x.png
--------------------------------------------------------------------------------
/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Notification_20pt_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Notification_20pt_3x.png
--------------------------------------------------------------------------------
/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29pt_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29pt_2x.png
--------------------------------------------------------------------------------
/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29pt_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29pt_3x.png
--------------------------------------------------------------------------------
/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40pt_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40pt_2x.png
--------------------------------------------------------------------------------
/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40pt_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/App/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40pt_3x.png
--------------------------------------------------------------------------------
/App/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/App/Resources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 |
28 | UIApplicationSupportsIndirectInputEvents
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 |
40 | UISupportedInterfaceOrientations~ipad
41 |
42 | UIInterfaceOrientationPortrait
43 | UIInterfaceOrientationPortraitUpsideDown
44 | UIInterfaceOrientationLandscapeLeft
45 | UIInterfaceOrientationLandscapeRight
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/App/Resources/InfoTests.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/App/Sources/App.swift:
--------------------------------------------------------------------------------
1 | import Color
2 | import Common
3 | import ComposableArchitecture
4 | import Preview
5 | import Shape
6 | import SwiftUI
7 | import Tabs
8 |
9 | @main
10 | struct App: SwiftUI.App {
11 | let store = Store(
12 | initialState: TabsState(
13 | selectedTab: .preview,
14 | color: ColorState(rgb: RGBColor(0.5, 0.5, 0.5)),
15 | shape: ShapeState(type: .square),
16 | preview: PreviewState()
17 | ),
18 | reducer: tabsReducer,
19 | environment: ()
20 | )
21 |
22 | var body: some Scene {
23 | WindowGroup {
24 | if !isRunningTests {
25 | TabsView(store: store)
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/App/Tests/AppTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import App
3 |
4 | final class AppTests: XCTestCase {
5 | func testExample() throws {
6 | XCTAssert(true)
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Color/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/Color/.swiftpm/xcode/xcshareddata/xcschemes/Color.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
54 |
55 |
61 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Color/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Color",
6 | platforms: [
7 | .iOS(.v14)
8 | ],
9 | products: [
10 | .library(
11 | name: "Color",
12 | targets: ["Color"]
13 | )
14 | ],
15 | dependencies: [
16 | .package(path: "../Common"),
17 | .package(path: "../Testing")
18 | ],
19 | targets: [
20 | .target(
21 | name: "Color",
22 | dependencies: ["Common"],
23 | path: "Sources"
24 | ),
25 | .testTarget(
26 | name: "ColorTests",
27 | dependencies: [
28 | "Color",
29 | "Testing"
30 | ],
31 | path: "Tests",
32 | exclude: ["__Snapshots__"]
33 | )
34 | ]
35 | )
36 |
--------------------------------------------------------------------------------
/Color/Sources/ColorAction.swift:
--------------------------------------------------------------------------------
1 | public enum ColorAction: Equatable {
2 | case didUpdateRed(Double)
3 | case didUpdateGreen(Double)
4 | case didUpdateBlue(Double)
5 | case apply
6 | }
7 |
--------------------------------------------------------------------------------
/Color/Sources/ColorReducer.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 |
3 | public let colorReducer = Reducer { state, action, _ in
4 | switch action {
5 | case let .didUpdateRed(value):
6 | state.rgb.red = max(0, min(1, value))
7 | return .none
8 |
9 | case let .didUpdateGreen(value):
10 | state.rgb.green = max(0, min(1, value))
11 | return .none
12 |
13 | case let .didUpdateBlue(value):
14 | state.rgb.blue = max(0, min(1, value))
15 | return .none
16 |
17 | case .apply:
18 | return .none
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Color/Sources/ColorState.swift:
--------------------------------------------------------------------------------
1 | public struct ColorState: Equatable {
2 | public init(rgb: RGBColor) {
3 | self.rgb = rgb
4 | }
5 |
6 | public var rgb: RGBColor
7 | }
8 |
--------------------------------------------------------------------------------
/Color/Sources/ColorView.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import SwiftUI
3 |
4 | public struct ColorView: View {
5 | public init(store: Store) {
6 | self.store = store
7 | }
8 |
9 | let store: Store
10 |
11 | public var body: some View {
12 | WithViewStore(store) { viewStore in
13 | VStack {
14 | VStack(alignment: .leading) {
15 | Text("Red")
16 |
17 | Slider(
18 | value: viewStore.binding(
19 | get: \.rgb.red,
20 | send: ColorAction.didUpdateRed
21 | ),
22 | in: (0...1)
23 | )
24 | .accentColor(.red)
25 |
26 | Text("Green")
27 |
28 | Slider(
29 | value: viewStore.binding(
30 | get: \.rgb.green,
31 | send: ColorAction.didUpdateGreen
32 | ),
33 | in: (0...1)
34 | )
35 | .accentColor(.green)
36 |
37 | Text("Blue")
38 |
39 | Slider(
40 | value: viewStore.binding(
41 | get: \.rgb.blue,
42 | send: ColorAction.didUpdateBlue
43 | ),
44 | in: (0...1)
45 | )
46 | .accentColor(.blue)
47 | }
48 | .padding()
49 | .border(
50 | Color(
51 | .displayP3,
52 | red: viewStore.rgb.red,
53 | green: viewStore.rgb.green,
54 | blue: viewStore.rgb.blue,
55 | opacity: 1
56 | ),
57 | width: 8
58 | )
59 | .padding()
60 |
61 | Button(action: { viewStore.send(.apply) }) {
62 | Text("Apply")
63 | .padding()
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
70 | struct ColorView_Previews: PreviewProvider {
71 | static var previews: some View {
72 | ColorView(store: Store(
73 | initialState: ColorState(rgb: RGBColor(0.5, 0.5, 0.5)),
74 | reducer: colorReducer,
75 | environment: ()
76 | ))
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Color/Sources/RGBColor.swift:
--------------------------------------------------------------------------------
1 | public struct RGBColor: Equatable {
2 | public init(_ red: Double, _ green: Double, _ blue: Double) {
3 | self.red = red
4 | self.green = green
5 | self.blue = blue
6 | }
7 |
8 | public var red: Double
9 | public var green: Double
10 | public var blue: Double
11 | }
12 |
--------------------------------------------------------------------------------
/Color/Tests/ColorTests.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import Testing
3 | import XCTest
4 | @testable import Color
5 |
6 | final class ColorTests: XCTestCase {
7 | func testUpdateColor() {
8 | let store = TestStore(
9 | initialState: ColorState(rgb: RGBColor(0, 0, 0)),
10 | reducer: colorReducer,
11 | environment: ()
12 | )
13 |
14 | store.assert(
15 | .send(.didUpdateRed(0.5)) {
16 | $0.rgb.red = 0.5
17 | },
18 | .send(.didUpdateGreen(0.5)) {
19 | $0.rgb.green = 0.5
20 | },
21 | .send(.didUpdateBlue(0.5)) {
22 | $0.rgb.blue = 0.5
23 | }
24 | )
25 | }
26 |
27 | func testUpdateColorWithOutOfBoundsValue() {
28 | let store = TestStore(
29 | initialState: ColorState(rgb: RGBColor(0.5, 0.5, 0.5)),
30 | reducer: colorReducer,
31 | environment: ()
32 | )
33 |
34 | store.assert(
35 | .send(.didUpdateRed(-0.1)) {
36 | $0.rgb.red = 0
37 | },
38 | .send(.didUpdateGreen(-0.1)) {
39 | $0.rgb.green = 0
40 | },
41 | .send(.didUpdateBlue(-0.1)) {
42 | $0.rgb.blue = 0
43 | },
44 | .send(.didUpdateRed(1.1)) {
45 | $0.rgb.red = 1
46 | },
47 | .send(.didUpdateGreen(1.1)) {
48 | $0.rgb.green = 1
49 | },
50 | .send(.didUpdateBlue(1.1)) {
51 | $0.rgb.blue = 1
52 | }
53 | )
54 | }
55 |
56 | func testApply() {
57 | let store = TestStore(
58 | initialState: ColorState(rgb: RGBColor(0, 0, 0)),
59 | reducer: colorReducer,
60 | environment: ()
61 | )
62 |
63 | store.assert(
64 | .send(.apply)
65 | )
66 | }
67 |
68 | func testPreviewSnapshot() {
69 | assertSnapshot(
70 | matching: ColorView_Previews.previews,
71 | layout: .device(config: .iPhoneXr)
72 | )
73 | }
74 |
75 | func testBlackColorSnapshot() {
76 | assertSnapshot(
77 | matching: ColorView(store: Store(
78 | initialState: ColorState(rgb: RGBColor(0, 0, 0)),
79 | reducer: .empty,
80 | environment: ()
81 | )),
82 | layout: .device(config: .iPhoneXr)
83 | )
84 | }
85 |
86 | func testWhiteColorSnapshot() {
87 | assertSnapshot(
88 | matching: ColorView(store: Store(
89 | initialState: ColorState(rgb: RGBColor(1, 1, 1)),
90 | reducer: .empty,
91 | environment: ()
92 | )),
93 | layout: .device(config: .iPhoneXr)
94 | )
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Color/Tests/__Snapshots__/ColorTests/testBlackColorSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Color/Tests/__Snapshots__/ColorTests/testBlackColorSnapshot.dark.png
--------------------------------------------------------------------------------
/Color/Tests/__Snapshots__/ColorTests/testBlackColorSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Color/Tests/__Snapshots__/ColorTests/testBlackColorSnapshot.light.png
--------------------------------------------------------------------------------
/Color/Tests/__Snapshots__/ColorTests/testPreviewSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Color/Tests/__Snapshots__/ColorTests/testPreviewSnapshot.dark.png
--------------------------------------------------------------------------------
/Color/Tests/__Snapshots__/ColorTests/testPreviewSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Color/Tests/__Snapshots__/ColorTests/testPreviewSnapshot.light.png
--------------------------------------------------------------------------------
/Color/Tests/__Snapshots__/ColorTests/testWhiteColorSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Color/Tests/__Snapshots__/ColorTests/testWhiteColorSnapshot.dark.png
--------------------------------------------------------------------------------
/Color/Tests/__Snapshots__/ColorTests/testWhiteColorSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Color/Tests/__Snapshots__/ColorTests/testWhiteColorSnapshot.light.png
--------------------------------------------------------------------------------
/Common/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/Common/.swiftpm/xcode/xcshareddata/xcschemes/Common.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
54 |
55 |
61 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Common/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Common",
6 | platforms: [
7 | .iOS(.v14)
8 | ],
9 | products: [
10 | .library(
11 | name: "Common",
12 | targets: ["Common"]
13 | )
14 | ],
15 | dependencies: [
16 | .package(
17 | url: "https://github.com/pointfreeco/swift-composable-architecture.git",
18 | from: "0.13.0"
19 | )
20 | ],
21 | targets: [
22 | .target(
23 | name: "Common",
24 | dependencies: [
25 | .product(
26 | name: "ComposableArchitecture",
27 | package: "swift-composable-architecture"
28 | )
29 | ],
30 | path: "Sources"
31 | ),
32 | .testTarget(
33 | name: "CommonTests",
34 | dependencies: ["Common"],
35 | path: "Tests"
36 | )
37 | ]
38 | )
39 |
--------------------------------------------------------------------------------
/Common/Sources/IsRunningTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public var isRunningTests: Bool {
4 | NSClassFromString("XCTestCase") != nil
5 | }
6 |
--------------------------------------------------------------------------------
/Common/Tests/IsRunningTestsTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Common
3 |
4 | final class IsRunningTestsTests: XCTestCase {
5 | func testShouldBeRunningTests() {
6 | XCTAssertTrue(isRunningTests)
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/ComposableApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 31516CDC25C6F9F9001CF6AE /* Color in Frameworks */ = {isa = PBXBuildFile; productRef = 31516CDB25C6F9F9001CF6AE /* Color */; };
11 | 31516CDE25C6F9F9001CF6AE /* Preview in Frameworks */ = {isa = PBXBuildFile; productRef = 31516CDD25C6F9F9001CF6AE /* Preview */; };
12 | 31516CE025C6F9F9001CF6AE /* Shape in Frameworks */ = {isa = PBXBuildFile; productRef = 31516CDF25C6F9F9001CF6AE /* Shape */; };
13 | 31516CE725C6FCB6001CF6AE /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 31516CE625C6FCB6001CF6AE /* Common */; };
14 | 31516D0025C717DE001CF6AE /* Tabs in Frameworks */ = {isa = PBXBuildFile; productRef = 31516CFF25C717DE001CF6AE /* Tabs */; };
15 | 31C9904D25C6CA9200E4A48E /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C9904C25C6CA9200E4A48E /* App.swift */; };
16 | 31C9905125C6CA9200E4A48E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 31C9905025C6CA9200E4A48E /* Assets.xcassets */; };
17 | 31C9905F25C6CA9300E4A48E /* AppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C9905E25C6CA9300E4A48E /* AppTests.swift */; };
18 | /* End PBXBuildFile section */
19 |
20 | /* Begin PBXContainerItemProxy section */
21 | 31C9905B25C6CA9300E4A48E /* PBXContainerItemProxy */ = {
22 | isa = PBXContainerItemProxy;
23 | containerPortal = 31C9904125C6CA9200E4A48E /* Project object */;
24 | proxyType = 1;
25 | remoteGlobalIDString = 31C9904825C6CA9200E4A48E;
26 | remoteInfo = App;
27 | };
28 | /* End PBXContainerItemProxy section */
29 |
30 | /* Begin PBXFileReference section */
31 | 31516CD225C6F9E5001CF6AE /* Color */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Color; sourceTree = ""; };
32 | 31516CD525C6F9EA001CF6AE /* Shape */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Shape; sourceTree = ""; };
33 | 31516CD825C6F9EE001CF6AE /* Preview */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Preview; sourceTree = ""; };
34 | 31516CE325C6FC91001CF6AE /* Common */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Common; sourceTree = ""; };
35 | 31516CFC25C717CC001CF6AE /* Tabs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Tabs; sourceTree = ""; };
36 | 31A8B50525C72D5B00CA3A14 /* Testing */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Testing; sourceTree = ""; };
37 | 31C9904925C6CA9200E4A48E /* ComposableApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ComposableApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
38 | 31C9904C25C6CA9200E4A48E /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
39 | 31C9905025C6CA9200E4A48E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
40 | 31C9905525C6CA9200E4A48E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
41 | 31C9905A25C6CA9300E4A48E /* AppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
42 | 31C9905E25C6CA9300E4A48E /* AppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTests.swift; sourceTree = ""; };
43 | 31C9906025C6CA9300E4A48E /* InfoTests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = InfoTests.plist; sourceTree = ""; };
44 | /* End PBXFileReference section */
45 |
46 | /* Begin PBXFrameworksBuildPhase section */
47 | 31C9904625C6CA9200E4A48E /* Frameworks */ = {
48 | isa = PBXFrameworksBuildPhase;
49 | buildActionMask = 2147483647;
50 | files = (
51 | 31516CE025C6F9F9001CF6AE /* Shape in Frameworks */,
52 | 31516CDC25C6F9F9001CF6AE /* Color in Frameworks */,
53 | 31516D0025C717DE001CF6AE /* Tabs in Frameworks */,
54 | 31516CDE25C6F9F9001CF6AE /* Preview in Frameworks */,
55 | 31516CE725C6FCB6001CF6AE /* Common in Frameworks */,
56 | );
57 | runOnlyForDeploymentPostprocessing = 0;
58 | };
59 | 31C9905725C6CA9300E4A48E /* Frameworks */ = {
60 | isa = PBXFrameworksBuildPhase;
61 | buildActionMask = 2147483647;
62 | files = (
63 | );
64 | runOnlyForDeploymentPostprocessing = 0;
65 | };
66 | /* End PBXFrameworksBuildPhase section */
67 |
68 | /* Begin PBXGroup section */
69 | 31516C9F25C6D005001CF6AE /* Frameworks */ = {
70 | isa = PBXGroup;
71 | children = (
72 | );
73 | name = Frameworks;
74 | sourceTree = "";
75 | };
76 | 31C9904025C6CA9200E4A48E = {
77 | isa = PBXGroup;
78 | children = (
79 | 31C9904B25C6CA9200E4A48E /* App */,
80 | 31516CE325C6FC91001CF6AE /* Common */,
81 | 31516CFC25C717CC001CF6AE /* Tabs */,
82 | 31516CD225C6F9E5001CF6AE /* Color */,
83 | 31516CD525C6F9EA001CF6AE /* Shape */,
84 | 31516CD825C6F9EE001CF6AE /* Preview */,
85 | 31A8B50525C72D5B00CA3A14 /* Testing */,
86 | 31C9904A25C6CA9200E4A48E /* Products */,
87 | 31516C9F25C6D005001CF6AE /* Frameworks */,
88 | );
89 | sourceTree = "";
90 | };
91 | 31C9904A25C6CA9200E4A48E /* Products */ = {
92 | isa = PBXGroup;
93 | children = (
94 | 31C9904925C6CA9200E4A48E /* ComposableApp.app */,
95 | 31C9905A25C6CA9300E4A48E /* AppTests.xctest */,
96 | );
97 | name = Products;
98 | sourceTree = "";
99 | };
100 | 31C9904B25C6CA9200E4A48E /* App */ = {
101 | isa = PBXGroup;
102 | children = (
103 | 31C9908825C6CAEF00E4A48E /* Resources */,
104 | 31C9908425C6CAE300E4A48E /* Sources */,
105 | 31C9908925C6CAF700E4A48E /* Tests */,
106 | );
107 | path = App;
108 | sourceTree = "";
109 | };
110 | 31C9908425C6CAE300E4A48E /* Sources */ = {
111 | isa = PBXGroup;
112 | children = (
113 | 31C9904C25C6CA9200E4A48E /* App.swift */,
114 | );
115 | path = Sources;
116 | sourceTree = "";
117 | };
118 | 31C9908825C6CAEF00E4A48E /* Resources */ = {
119 | isa = PBXGroup;
120 | children = (
121 | 31C9905025C6CA9200E4A48E /* Assets.xcassets */,
122 | 31C9905525C6CA9200E4A48E /* Info.plist */,
123 | 31C9906025C6CA9300E4A48E /* InfoTests.plist */,
124 | );
125 | path = Resources;
126 | sourceTree = "";
127 | };
128 | 31C9908925C6CAF700E4A48E /* Tests */ = {
129 | isa = PBXGroup;
130 | children = (
131 | 31C9905E25C6CA9300E4A48E /* AppTests.swift */,
132 | );
133 | path = Tests;
134 | sourceTree = "";
135 | };
136 | /* End PBXGroup section */
137 |
138 | /* Begin PBXNativeTarget section */
139 | 31C9904825C6CA9200E4A48E /* App */ = {
140 | isa = PBXNativeTarget;
141 | buildConfigurationList = 31C9906E25C6CA9300E4A48E /* Build configuration list for PBXNativeTarget "App" */;
142 | buildPhases = (
143 | 31C9904525C6CA9200E4A48E /* Sources */,
144 | 31C9904625C6CA9200E4A48E /* Frameworks */,
145 | 31C9904725C6CA9200E4A48E /* Resources */,
146 | );
147 | buildRules = (
148 | );
149 | dependencies = (
150 | );
151 | name = App;
152 | packageProductDependencies = (
153 | 31516CDB25C6F9F9001CF6AE /* Color */,
154 | 31516CDD25C6F9F9001CF6AE /* Preview */,
155 | 31516CDF25C6F9F9001CF6AE /* Shape */,
156 | 31516CE625C6FCB6001CF6AE /* Common */,
157 | 31516CFF25C717DE001CF6AE /* Tabs */,
158 | );
159 | productName = App;
160 | productReference = 31C9904925C6CA9200E4A48E /* ComposableApp.app */;
161 | productType = "com.apple.product-type.application";
162 | };
163 | 31C9905925C6CA9300E4A48E /* AppTests */ = {
164 | isa = PBXNativeTarget;
165 | buildConfigurationList = 31C9907125C6CA9300E4A48E /* Build configuration list for PBXNativeTarget "AppTests" */;
166 | buildPhases = (
167 | 31C9905625C6CA9300E4A48E /* Sources */,
168 | 31C9905725C6CA9300E4A48E /* Frameworks */,
169 | 31C9905825C6CA9300E4A48E /* Resources */,
170 | );
171 | buildRules = (
172 | );
173 | dependencies = (
174 | 31C9905C25C6CA9300E4A48E /* PBXTargetDependency */,
175 | );
176 | name = AppTests;
177 | productName = AppTests;
178 | productReference = 31C9905A25C6CA9300E4A48E /* AppTests.xctest */;
179 | productType = "com.apple.product-type.bundle.unit-test";
180 | };
181 | /* End PBXNativeTarget section */
182 |
183 | /* Begin PBXProject section */
184 | 31C9904125C6CA9200E4A48E /* Project object */ = {
185 | isa = PBXProject;
186 | attributes = {
187 | LastSwiftUpdateCheck = 1240;
188 | LastUpgradeCheck = 1240;
189 | TargetAttributes = {
190 | 31C9904825C6CA9200E4A48E = {
191 | CreatedOnToolsVersion = 12.4;
192 | };
193 | 31C9905925C6CA9300E4A48E = {
194 | CreatedOnToolsVersion = 12.4;
195 | TestTargetID = 31C9904825C6CA9200E4A48E;
196 | };
197 | };
198 | };
199 | buildConfigurationList = 31C9904425C6CA9200E4A48E /* Build configuration list for PBXProject "ComposableApp" */;
200 | compatibilityVersion = "Xcode 9.3";
201 | developmentRegion = en;
202 | hasScannedForEncodings = 0;
203 | knownRegions = (
204 | en,
205 | Base,
206 | );
207 | mainGroup = 31C9904025C6CA9200E4A48E;
208 | productRefGroup = 31C9904A25C6CA9200E4A48E /* Products */;
209 | projectDirPath = "";
210 | projectRoot = "";
211 | targets = (
212 | 31C9904825C6CA9200E4A48E /* App */,
213 | 31C9905925C6CA9300E4A48E /* AppTests */,
214 | );
215 | };
216 | /* End PBXProject section */
217 |
218 | /* Begin PBXResourcesBuildPhase section */
219 | 31C9904725C6CA9200E4A48E /* Resources */ = {
220 | isa = PBXResourcesBuildPhase;
221 | buildActionMask = 2147483647;
222 | files = (
223 | 31C9905125C6CA9200E4A48E /* Assets.xcassets in Resources */,
224 | );
225 | runOnlyForDeploymentPostprocessing = 0;
226 | };
227 | 31C9905825C6CA9300E4A48E /* Resources */ = {
228 | isa = PBXResourcesBuildPhase;
229 | buildActionMask = 2147483647;
230 | files = (
231 | );
232 | runOnlyForDeploymentPostprocessing = 0;
233 | };
234 | /* End PBXResourcesBuildPhase section */
235 |
236 | /* Begin PBXSourcesBuildPhase section */
237 | 31C9904525C6CA9200E4A48E /* Sources */ = {
238 | isa = PBXSourcesBuildPhase;
239 | buildActionMask = 2147483647;
240 | files = (
241 | 31C9904D25C6CA9200E4A48E /* App.swift in Sources */,
242 | );
243 | runOnlyForDeploymentPostprocessing = 0;
244 | };
245 | 31C9905625C6CA9300E4A48E /* Sources */ = {
246 | isa = PBXSourcesBuildPhase;
247 | buildActionMask = 2147483647;
248 | files = (
249 | 31C9905F25C6CA9300E4A48E /* AppTests.swift in Sources */,
250 | );
251 | runOnlyForDeploymentPostprocessing = 0;
252 | };
253 | /* End PBXSourcesBuildPhase section */
254 |
255 | /* Begin PBXTargetDependency section */
256 | 31C9905C25C6CA9300E4A48E /* PBXTargetDependency */ = {
257 | isa = PBXTargetDependency;
258 | target = 31C9904825C6CA9200E4A48E /* App */;
259 | targetProxy = 31C9905B25C6CA9300E4A48E /* PBXContainerItemProxy */;
260 | };
261 | /* End PBXTargetDependency section */
262 |
263 | /* Begin XCBuildConfiguration section */
264 | 31C9906C25C6CA9300E4A48E /* Debug */ = {
265 | isa = XCBuildConfiguration;
266 | buildSettings = {
267 | ALWAYS_SEARCH_USER_PATHS = NO;
268 | CLANG_ANALYZER_NONNULL = YES;
269 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
270 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
271 | CLANG_CXX_LIBRARY = "libc++";
272 | CLANG_ENABLE_MODULES = YES;
273 | CLANG_ENABLE_OBJC_ARC = YES;
274 | CLANG_ENABLE_OBJC_WEAK = YES;
275 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
276 | CLANG_WARN_BOOL_CONVERSION = YES;
277 | CLANG_WARN_COMMA = YES;
278 | CLANG_WARN_CONSTANT_CONVERSION = YES;
279 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
280 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
281 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
282 | CLANG_WARN_EMPTY_BODY = YES;
283 | CLANG_WARN_ENUM_CONVERSION = YES;
284 | CLANG_WARN_INFINITE_RECURSION = YES;
285 | CLANG_WARN_INT_CONVERSION = YES;
286 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
287 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
288 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
289 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
290 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
291 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
292 | CLANG_WARN_STRICT_PROTOTYPES = YES;
293 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
294 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
295 | CLANG_WARN_UNREACHABLE_CODE = YES;
296 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
297 | COPY_PHASE_STRIP = NO;
298 | CURRENT_PROJECT_VERSION = 0;
299 | DEBUG_INFORMATION_FORMAT = dwarf;
300 | ENABLE_STRICT_OBJC_MSGSEND = YES;
301 | ENABLE_TESTABILITY = YES;
302 | GCC_C_LANGUAGE_STANDARD = gnu11;
303 | GCC_DYNAMIC_NO_PIC = NO;
304 | GCC_NO_COMMON_BLOCKS = YES;
305 | GCC_OPTIMIZATION_LEVEL = 0;
306 | GCC_PREPROCESSOR_DEFINITIONS = (
307 | "DEBUG=1",
308 | "$(inherited)",
309 | );
310 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
311 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
312 | GCC_WARN_UNDECLARED_SELECTOR = YES;
313 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
314 | GCC_WARN_UNUSED_FUNCTION = YES;
315 | GCC_WARN_UNUSED_VARIABLE = YES;
316 | IPHONEOS_DEPLOYMENT_TARGET = 14.4;
317 | MARKETING_VERSION = 1.0.0;
318 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
319 | MTL_FAST_MATH = YES;
320 | ONLY_ACTIVE_ARCH = YES;
321 | SDKROOT = iphoneos;
322 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
323 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
324 | };
325 | name = Debug;
326 | };
327 | 31C9906D25C6CA9300E4A48E /* Release */ = {
328 | isa = XCBuildConfiguration;
329 | buildSettings = {
330 | ALWAYS_SEARCH_USER_PATHS = NO;
331 | CLANG_ANALYZER_NONNULL = YES;
332 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
333 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
334 | CLANG_CXX_LIBRARY = "libc++";
335 | CLANG_ENABLE_MODULES = YES;
336 | CLANG_ENABLE_OBJC_ARC = YES;
337 | CLANG_ENABLE_OBJC_WEAK = YES;
338 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
339 | CLANG_WARN_BOOL_CONVERSION = YES;
340 | CLANG_WARN_COMMA = YES;
341 | CLANG_WARN_CONSTANT_CONVERSION = YES;
342 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
343 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
344 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
345 | CLANG_WARN_EMPTY_BODY = YES;
346 | CLANG_WARN_ENUM_CONVERSION = YES;
347 | CLANG_WARN_INFINITE_RECURSION = YES;
348 | CLANG_WARN_INT_CONVERSION = YES;
349 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
350 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
351 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
352 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
353 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
354 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
355 | CLANG_WARN_STRICT_PROTOTYPES = YES;
356 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
357 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
358 | CLANG_WARN_UNREACHABLE_CODE = YES;
359 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
360 | COPY_PHASE_STRIP = NO;
361 | CURRENT_PROJECT_VERSION = 0;
362 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
363 | ENABLE_NS_ASSERTIONS = NO;
364 | ENABLE_STRICT_OBJC_MSGSEND = YES;
365 | GCC_C_LANGUAGE_STANDARD = gnu11;
366 | GCC_NO_COMMON_BLOCKS = YES;
367 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
368 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
369 | GCC_WARN_UNDECLARED_SELECTOR = YES;
370 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
371 | GCC_WARN_UNUSED_FUNCTION = YES;
372 | GCC_WARN_UNUSED_VARIABLE = YES;
373 | IPHONEOS_DEPLOYMENT_TARGET = 14.4;
374 | MARKETING_VERSION = 1.0.0;
375 | MTL_ENABLE_DEBUG_INFO = NO;
376 | MTL_FAST_MATH = YES;
377 | SDKROOT = iphoneos;
378 | SWIFT_COMPILATION_MODE = wholemodule;
379 | SWIFT_OPTIMIZATION_LEVEL = "-O";
380 | VALIDATE_PRODUCT = YES;
381 | };
382 | name = Release;
383 | };
384 | 31C9906F25C6CA9300E4A48E /* Debug */ = {
385 | isa = XCBuildConfiguration;
386 | buildSettings = {
387 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
388 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
389 | CODE_SIGN_STYLE = Automatic;
390 | ENABLE_PREVIEWS = YES;
391 | INFOPLIST_FILE = App/Resources/Info.plist;
392 | LD_RUNPATH_SEARCH_PATHS = (
393 | "$(inherited)",
394 | "@executable_path/Frameworks",
395 | );
396 | PRODUCT_BUNDLE_IDENTIFIER = pl.darrarski.ComposableApp;
397 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
398 | PRODUCT_NAME = ComposableApp;
399 | SWIFT_VERSION = 5.0;
400 | TARGETED_DEVICE_FAMILY = 1;
401 | };
402 | name = Debug;
403 | };
404 | 31C9907025C6CA9300E4A48E /* Release */ = {
405 | isa = XCBuildConfiguration;
406 | buildSettings = {
407 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
408 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
409 | CODE_SIGN_STYLE = Automatic;
410 | ENABLE_PREVIEWS = YES;
411 | INFOPLIST_FILE = App/Resources/Info.plist;
412 | LD_RUNPATH_SEARCH_PATHS = (
413 | "$(inherited)",
414 | "@executable_path/Frameworks",
415 | );
416 | PRODUCT_BUNDLE_IDENTIFIER = pl.darrarski.ComposableApp;
417 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
418 | PRODUCT_NAME = ComposableApp;
419 | SWIFT_VERSION = 5.0;
420 | TARGETED_DEVICE_FAMILY = 1;
421 | };
422 | name = Release;
423 | };
424 | 31C9907225C6CA9300E4A48E /* Debug */ = {
425 | isa = XCBuildConfiguration;
426 | buildSettings = {
427 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
428 | BUNDLE_LOADER = "$(TEST_HOST)";
429 | CODE_SIGN_STYLE = Automatic;
430 | INFOPLIST_FILE = App/Resources/InfoTests.plist;
431 | LD_RUNPATH_SEARCH_PATHS = (
432 | "$(inherited)",
433 | "@executable_path/Frameworks",
434 | "@loader_path/Frameworks",
435 | );
436 | PRODUCT_BUNDLE_IDENTIFIER = pl.darrarski.ComposableAppTests;
437 | PRODUCT_NAME = "$(TARGET_NAME)";
438 | SWIFT_VERSION = 5.0;
439 | TARGETED_DEVICE_FAMILY = "1,2";
440 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ComposableApp.app/ComposableApp";
441 | };
442 | name = Debug;
443 | };
444 | 31C9907325C6CA9300E4A48E /* Release */ = {
445 | isa = XCBuildConfiguration;
446 | buildSettings = {
447 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
448 | BUNDLE_LOADER = "$(TEST_HOST)";
449 | CODE_SIGN_STYLE = Automatic;
450 | INFOPLIST_FILE = App/Resources/InfoTests.plist;
451 | LD_RUNPATH_SEARCH_PATHS = (
452 | "$(inherited)",
453 | "@executable_path/Frameworks",
454 | "@loader_path/Frameworks",
455 | );
456 | PRODUCT_BUNDLE_IDENTIFIER = pl.darrarski.ComposableAppTests;
457 | PRODUCT_NAME = "$(TARGET_NAME)";
458 | SWIFT_VERSION = 5.0;
459 | TARGETED_DEVICE_FAMILY = "1,2";
460 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ComposableApp.app/ComposableApp";
461 | };
462 | name = Release;
463 | };
464 | /* End XCBuildConfiguration section */
465 |
466 | /* Begin XCConfigurationList section */
467 | 31C9904425C6CA9200E4A48E /* Build configuration list for PBXProject "ComposableApp" */ = {
468 | isa = XCConfigurationList;
469 | buildConfigurations = (
470 | 31C9906C25C6CA9300E4A48E /* Debug */,
471 | 31C9906D25C6CA9300E4A48E /* Release */,
472 | );
473 | defaultConfigurationIsVisible = 0;
474 | defaultConfigurationName = Release;
475 | };
476 | 31C9906E25C6CA9300E4A48E /* Build configuration list for PBXNativeTarget "App" */ = {
477 | isa = XCConfigurationList;
478 | buildConfigurations = (
479 | 31C9906F25C6CA9300E4A48E /* Debug */,
480 | 31C9907025C6CA9300E4A48E /* Release */,
481 | );
482 | defaultConfigurationIsVisible = 0;
483 | defaultConfigurationName = Release;
484 | };
485 | 31C9907125C6CA9300E4A48E /* Build configuration list for PBXNativeTarget "AppTests" */ = {
486 | isa = XCConfigurationList;
487 | buildConfigurations = (
488 | 31C9907225C6CA9300E4A48E /* Debug */,
489 | 31C9907325C6CA9300E4A48E /* Release */,
490 | );
491 | defaultConfigurationIsVisible = 0;
492 | defaultConfigurationName = Release;
493 | };
494 | /* End XCConfigurationList section */
495 |
496 | /* Begin XCSwiftPackageProductDependency section */
497 | 31516CDB25C6F9F9001CF6AE /* Color */ = {
498 | isa = XCSwiftPackageProductDependency;
499 | productName = Color;
500 | };
501 | 31516CDD25C6F9F9001CF6AE /* Preview */ = {
502 | isa = XCSwiftPackageProductDependency;
503 | productName = Preview;
504 | };
505 | 31516CDF25C6F9F9001CF6AE /* Shape */ = {
506 | isa = XCSwiftPackageProductDependency;
507 | productName = Shape;
508 | };
509 | 31516CE625C6FCB6001CF6AE /* Common */ = {
510 | isa = XCSwiftPackageProductDependency;
511 | productName = Common;
512 | };
513 | 31516CFF25C717DE001CF6AE /* Tabs */ = {
514 | isa = XCSwiftPackageProductDependency;
515 | productName = Tabs;
516 | };
517 | /* End XCSwiftPackageProductDependency section */
518 | };
519 | rootObject = 31C9904125C6CA9200E4A48E /* Project object */;
520 | }
521 |
--------------------------------------------------------------------------------
/ComposableApp.xcodeproj/xcshareddata/xcschemes/ComposableApp.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
71 |
73 |
79 |
80 |
81 |
82 |
84 |
85 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/ComposableApp.xcodeproj/xcshareddata/xcschemes/Tests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
16 |
18 |
24 |
25 |
26 |
28 |
34 |
35 |
36 |
38 |
44 |
45 |
46 |
48 |
54 |
55 |
56 |
58 |
64 |
65 |
66 |
68 |
74 |
75 |
76 |
77 |
78 |
88 |
89 |
95 |
96 |
98 |
99 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Dariusz Rybicki Darrarski
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Misc/color_screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Misc/color_screen.png
--------------------------------------------------------------------------------
/Misc/preview_screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Misc/preview_screen.png
--------------------------------------------------------------------------------
/Misc/shape_screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Misc/shape_screen.png
--------------------------------------------------------------------------------
/Preview/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/Preview/.swiftpm/xcode/xcshareddata/xcschemes/Preview.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
54 |
55 |
61 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Preview/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Preview",
6 | platforms: [
7 | .iOS(.v14)
8 | ],
9 | products: [
10 | .library(
11 | name: "Preview",
12 | targets: ["Preview"]
13 | )
14 | ],
15 | dependencies: [
16 | .package(path: "../Common"),
17 | .package(path: "../Color"),
18 | .package(path: "../Shape"),
19 | .package(path: "../Testing")
20 | ],
21 | targets: [
22 | .target(
23 | name: "Preview",
24 | dependencies: [
25 | "Common",
26 | "Color",
27 | "Shape"
28 | ],
29 | path: "Sources"
30 | ),
31 | .testTarget(
32 | name: "PreviewTests",
33 | dependencies: [
34 | "Preview",
35 | "Testing"
36 | ],
37 | path: "Tests",
38 | exclude: ["__Snapshots__"]
39 | )
40 | ]
41 | )
42 |
--------------------------------------------------------------------------------
/Preview/Sources/PreviewAction.swift:
--------------------------------------------------------------------------------
1 | public enum PreviewAction: Equatable {
2 | case reset
3 | }
4 |
--------------------------------------------------------------------------------
/Preview/Sources/PreviewReducer.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 |
3 | public let previewReducer = Reducer { state, action, _ in
4 | switch action {
5 | case .reset:
6 | state.color = nil
7 | state.shape = nil
8 | return .none
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Preview/Sources/PreviewState.swift:
--------------------------------------------------------------------------------
1 | import Color
2 | import Shape
3 |
4 | public struct PreviewState: Equatable {
5 | public init(color: RGBColor? = nil, shape: ShapeType? = nil) {
6 | self.color = color
7 | self.shape = shape
8 | }
9 |
10 | public var color: RGBColor?
11 | public var shape: ShapeType?
12 | }
13 |
--------------------------------------------------------------------------------
/Preview/Sources/PreviewView.swift:
--------------------------------------------------------------------------------
1 | import Color
2 | import ComposableArchitecture
3 | import Shape
4 | import SwiftUI
5 |
6 | public struct PreviewView: View {
7 | public init(store: Store) {
8 | self.store = store
9 | }
10 |
11 | let store: Store
12 |
13 | public var body: some View {
14 | WithViewStore(store) { viewStore in
15 | if let color = viewStore.color,
16 | let shape = viewStore.shape {
17 | VStack {
18 | Group {
19 | switch shape {
20 | case .circle:
21 | Circle()
22 |
23 | case .square:
24 | Rectangle()
25 | }
26 | }
27 | .aspectRatio(1, contentMode: .fit)
28 | .padding()
29 | .foregroundColor(Color(
30 | .displayP3,
31 | red: color.red,
32 | green: color.green,
33 | blue: color.blue,
34 | opacity: 1
35 | ))
36 |
37 | Button(action: { viewStore.send(.reset) }) {
38 | Text("Reset")
39 | .padding()
40 | }
41 | }
42 | } else {
43 | VStack {
44 | Text("No preview").font(.title)
45 | Text("Apply color and shape first")
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
52 | struct PreviewView_Previews: PreviewProvider {
53 | static var previews: some View {
54 | PreviewView(store: Store(
55 | initialState: PreviewState(
56 | color: RGBColor(0.5, 0.5, 0.5),
57 | shape: .circle
58 | ),
59 | reducer: previewReducer,
60 | environment: ()
61 | ))
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Preview/Tests/PreviewTests.swift:
--------------------------------------------------------------------------------
1 | import Color
2 | import ComposableArchitecture
3 | import Shape
4 | import Testing
5 | import XCTest
6 | @testable import Preview
7 |
8 | final class PreviewTests: XCTestCase {
9 | func testReset() {
10 | let store = TestStore(
11 | initialState: PreviewState(
12 | color: RGBColor(0, 0, 0),
13 | shape: .circle
14 | ),
15 | reducer: previewReducer,
16 | environment: ()
17 | )
18 |
19 | store.assert(
20 | .send(.reset) {
21 | $0.color = nil
22 | $0.shape = nil
23 | }
24 | )
25 | }
26 |
27 | func testPreviewSnapshot() {
28 | assertSnapshot(
29 | matching: PreviewView_Previews.previews,
30 | layout: .device(config: .iPhoneXr)
31 | )
32 | }
33 |
34 | func testEmptySnapshot() {
35 | assertSnapshot(
36 | matching: PreviewView(store: Store(
37 | initialState: PreviewState(),
38 | reducer: .empty,
39 | environment: ()
40 | )),
41 | layout: .device(config: .iPhoneXr)
42 | )
43 | }
44 |
45 | func testCircleSnapshot() {
46 | assertSnapshot(
47 | matching: PreviewView(store: Store(
48 | initialState: PreviewState(
49 | color: RGBColor(0.25, 0.5, 0.33),
50 | shape: .circle
51 | ),
52 | reducer: .empty,
53 | environment: ()
54 | )),
55 | layout: .device(config: .iPhoneXr)
56 | )
57 | }
58 |
59 | func testSquareSnapshot() {
60 | assertSnapshot(
61 | matching: PreviewView(store: Store(
62 | initialState: PreviewState(
63 | color: RGBColor(0.75, 0.5, 0.25),
64 | shape: .square
65 | ),
66 | reducer: .empty,
67 | environment: ()
68 | )),
69 | layout: .device(config: .iPhoneXr)
70 | )
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Preview/Tests/__Snapshots__/PreviewTests/testCircleSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Preview/Tests/__Snapshots__/PreviewTests/testCircleSnapshot.dark.png
--------------------------------------------------------------------------------
/Preview/Tests/__Snapshots__/PreviewTests/testCircleSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Preview/Tests/__Snapshots__/PreviewTests/testCircleSnapshot.light.png
--------------------------------------------------------------------------------
/Preview/Tests/__Snapshots__/PreviewTests/testEmptySnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Preview/Tests/__Snapshots__/PreviewTests/testEmptySnapshot.dark.png
--------------------------------------------------------------------------------
/Preview/Tests/__Snapshots__/PreviewTests/testEmptySnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Preview/Tests/__Snapshots__/PreviewTests/testEmptySnapshot.light.png
--------------------------------------------------------------------------------
/Preview/Tests/__Snapshots__/PreviewTests/testPreviewSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Preview/Tests/__Snapshots__/PreviewTests/testPreviewSnapshot.dark.png
--------------------------------------------------------------------------------
/Preview/Tests/__Snapshots__/PreviewTests/testPreviewSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Preview/Tests/__Snapshots__/PreviewTests/testPreviewSnapshot.light.png
--------------------------------------------------------------------------------
/Preview/Tests/__Snapshots__/PreviewTests/testSquareSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Preview/Tests/__Snapshots__/PreviewTests/testSquareSnapshot.dark.png
--------------------------------------------------------------------------------
/Preview/Tests/__Snapshots__/PreviewTests/testSquareSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Preview/Tests/__Snapshots__/PreviewTests/testSquareSnapshot.light.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Swift Composable App Example
2 |
3 | 
4 | 
5 | 
6 |
7 | |1️⃣ Adjust color|2️⃣ Select shape|3️⃣ Preview result|
8 | |:--|:--|:--|
9 | ||||
10 |
11 | ## 📝 Description
12 |
13 | This repository contains an example iOS app built with module composition in mind. It shows how to organize source code into separate modules, where each of them is responsible for providing different functionality in the app.
14 |
15 | Instead of splitting the code into separate Xcode's targets that produce frameworks, this project uses Swift Package Manager support in Xcode. There is a single iOS application target in the Xcode project (with corresponding unit tests target) and several Swift packages for each corresponding app feature. Some of the advantages of this setup are:
16 |
17 | 👍 **Easier to maintain project structure**. Swift packages do not contain massive configuration in the form of `.pbxproj` file. The disk's file structure is what you work on in Xcode, and it stays in sync with it. Files are sorted alphabetically. Every folder on the disk automatically becomes a group in Xcode.
18 |
19 | 👍 **Better cooperation in a team**. Because there are no entries in `.pbxproj` for every newly added or moved file, you can avoid hard to resolve conflicts in a version control system.
20 |
21 | 👍 **Easier code reusability**. You can easily extract the package and use it in another project. Everything you need is to copy the package directory from one project to another.
22 |
23 | 👍 **Easier to manage dependencies** (as long as you use Swift Package Manager). All the libraries your code depends on are defined in `Package.swift` file. Linking between your libraries is also easy and defined in the same file. The only thing you have to do in Xcode is embedding all your package libraries in the app target.
24 |
25 | 👍 **Easier module separation**. It's straightforward to create a new module and extract a piece of your source code to it. You can focus on the app architecture and save the time you would spend creating and configuring Xcode targets. As a result, you can achieve better composition and separation in your codebase.
26 |
27 | ## 🛠 Tech Stack
28 |
29 | - [Xcode](https://developer.apple.com/xcode/) 12.4
30 | - [Swift](https://swift.org/) 5.3
31 |
32 | ### 🧰 Frameworks
33 |
34 | - [SwiftUI](https://developer.apple.com/documentation/swiftui)
35 | - [ComposableArchitecture](http://github.com/pointfreeco/swift-composable-architecture/)
36 | - [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing)
37 |
38 | ## 🚀 Quick start
39 |
40 | Open `ComposableApp.xcodeproj` in Xcode. You can run the app using the `ComposableApp` build scheme. Other schemes are for building and testing individual modules of the project.
41 |
42 | ## 🏛 Architecture
43 |
44 | The following diagram explains how the project is structured:
45 |
46 | ```
47 | +---------------+ +--------------+ +---------------+
48 | | App +<--------------+ Common +<--+---+ Library A |
49 | +----+-----+----+ +---+---+---+--+ | +---------------+
50 | ^ ^ | | | |
51 | | | +---------------+ | | | | +---------------+
52 | | +----+ Feature A +<--+ | | +---+ Library B |
53 | | +---------------+ | | | +---------------+
54 | | | | |
55 | | +---------------+ | | | +---------------+
56 | +----------+ Feature B +<------+ | +---+ Library C |
57 | +---------------+ | +---------------+
58 | ^ |
59 | | |
60 | +---------------+ |
61 | + Feature C +<----------+
62 | +---------------+
63 | ```
64 |
65 | - **App** - The application module
66 | - **Feature** - Feature-specific module
67 | - **Common** - A module containing shared logic and linking external dependencies
68 | - **Library** - External dependency
69 | - **A ← B** - Module A depends on module B
70 |
71 | For easy module separation this project uses [ComposableArchitecture](http://github.com/pointfreeco/swift-composable-architecture/) library by [Point-Free](https://www.pointfree.co).
72 |
73 | ### 🎯 Xcode project
74 |
75 | |Target|Kind|Description|
76 | |:--|:--|:--|
77 | |`App`|iOS app|Main target of the app.|
78 | |`AppTests`|iOS unit tests target|Unit tests of the app target.|
79 |
80 | All Swift package libraries are embedded in the `App` target. The only exception is the `Testing` library used by unit test targets only and not embedded in the app.
81 |
82 | ### 📦 Common package
83 |
84 | |Target|Kind|Description|
85 | |:--|:--|:--|
86 | |`Common`|library|Contains shared logic and links external dependencies.|
87 | |`CommonTests`|unit tests|Shared logic tests.|
88 |
89 | Every other Swift package library in the project depends on the `Common` library. The purpose of this library is to contain source code that can be shared across the whole codebase and to link external libraries. It's a single place where external dependencies are defined.
90 |
91 | ### 📦 Tabs package
92 |
93 | |Target|Kind|Description|
94 | |:--|:--|:--|
95 | |`Tabs`|library|Contains tabbed user interface with screens defined in other libraries.|
96 | |`TabsTests`|unit tests|Unit and snapshot tests of the tabbed user interface.|
97 |
98 | `Tabs` library depends on `Color`, `Shape` and `Preview` libraries, which contain screens presented in the tabbed user interface. It also binds the logic of these screens and handles communication between them.
99 |
100 | ### 📦 Color package
101 |
102 | |Target|Kind|Description|
103 | |:--|:--|:--|
104 | |`Color`|library|Contains color picker screen.|
105 | |`ColorTests`|unit tests|Unit and snapshot tests of the color picker screen.|
106 |
107 | This package is a feature module that contains a color picker screen in isolation from other modules.
108 |
109 | ### 📦 Shape package
110 |
111 | |Target|Kind|Description|
112 | |:--|:--|:--|
113 | |`Shape`|library|Contains shape picker screen.|
114 | |`ShapeTests`|unit tests|Unit and snapshot tests of the shape picker screen.|
115 |
116 | This package is a feature module that contains a shape picker screen in isolation from other modules.
117 |
118 | ### 📦 Preview package
119 |
120 | |Target|Kind|Description|
121 | |:--|:--|:--|
122 | |`Preview`|library|Contains screen with a preview of a shape in a given color.|
123 | |`PreviewTests`|unit tests|Unit and snapshot tests of the preview screen.|
124 |
125 | This package is a feature module that contains a shape preview screen. `Preview` library depends on `Color` and `Shape` libraries and uses models defined by them.
126 |
127 | ### 📦 Testing package
128 |
129 | |Target|Kind|Description|
130 | |:--|:--|:--|
131 | |`Testing`|library|Contains shared testing logic and links external test dependencies.|
132 |
133 | Every other Swift package unit tests target depends on the `Testing` library. This way, it's a single place where external test dependencies are defined.
134 |
135 | ## 🧪 Tests
136 |
137 | There is a unit testing target that tests the main app target. This one is managed by Xcode in a traditional way. All Swift packages in the project contain a test target in addition to a library product target. You can run tests for them from Xcode by selecting a given module build scheme.
138 |
139 | The project contains **snapshot tests** of SwiftUI views. These were recorded using iPhone 12 simulator and should be run using it. Otherwise, you can experience snapshot test failures caused by screen resolution mismatch.
140 |
141 | ## ☕️ Do you like the project?
142 |
143 |
144 |
145 | ## 📄 License
146 |
147 | Copyright © 2021 [Dariusz Rybicki Darrarski](http://darrarski.pl)
148 |
149 | License: [MIT](LICENSE)
150 |
--------------------------------------------------------------------------------
/Shape/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/Shape/.swiftpm/xcode/xcshareddata/xcschemes/Shape.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
54 |
55 |
61 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Shape/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Shape",
6 | platforms: [
7 | .iOS(.v14)
8 | ],
9 | products: [
10 | .library(
11 | name: "Shape",
12 | targets: ["Shape"]
13 | )
14 | ],
15 | dependencies: [
16 | .package(path: "../Common"),
17 | .package(path: "../Testing")
18 | ],
19 | targets: [
20 | .target(
21 | name: "Shape",
22 | dependencies: ["Common"],
23 | path: "Sources"
24 | ),
25 | .testTarget(
26 | name: "ShapeTests",
27 | dependencies: [
28 | "Shape",
29 | "Testing"
30 | ],
31 | path: "Tests",
32 | exclude: ["__Snapshots__"]
33 | )
34 | ]
35 | )
36 |
--------------------------------------------------------------------------------
/Shape/Sources/ShapeAction.swift:
--------------------------------------------------------------------------------
1 | public enum ShapeAction: Equatable {
2 | case didSelectType(ShapeType)
3 | case apply
4 | }
5 |
--------------------------------------------------------------------------------
/Shape/Sources/ShapeReducer.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 |
3 | public let shapeReducer = Reducer { state, action, _ in
4 | switch action {
5 | case let .didSelectType(value):
6 | state.type = value
7 | return .none
8 |
9 | case .apply:
10 | return .none
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Shape/Sources/ShapeState.swift:
--------------------------------------------------------------------------------
1 | public struct ShapeState: Equatable {
2 | public init(type: ShapeType) {
3 | self.type = type
4 | }
5 |
6 | public var type: ShapeType
7 | }
8 |
--------------------------------------------------------------------------------
/Shape/Sources/ShapeType.swift:
--------------------------------------------------------------------------------
1 | public enum ShapeType: Equatable {
2 | case square
3 | case circle
4 | }
5 |
--------------------------------------------------------------------------------
/Shape/Sources/ShapeView.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import SwiftUI
3 |
4 | public struct ShapeView: View {
5 | public init(store: Store) {
6 | self.store = store
7 | }
8 |
9 | let store: Store
10 |
11 | public var body: some View {
12 | WithViewStore(store) { viewStore in
13 | VStack {
14 | HStack {
15 | Spacer()
16 |
17 | Button(action: { viewStore.send(.didSelectType(.square)) }) {
18 | Rectangle()
19 | .stroke(lineWidth: 4)
20 | .frame(width: 50, height: 50)
21 | .overlay(Group {
22 | if viewStore.type == .square {
23 | Image(systemName: "checkmark")
24 | .imageScale(.large)
25 | } else {
26 | EmptyView()
27 | }
28 | })
29 | .padding()
30 | }
31 |
32 | Spacer()
33 |
34 | Button(action: { viewStore.send(.didSelectType(.circle)) }) {
35 | Circle()
36 | .stroke(lineWidth: 4)
37 | .frame(width: 50, height: 50)
38 | .overlay(Group {
39 | if viewStore.type == .circle {
40 | Image(systemName: "checkmark")
41 | .imageScale(.large)
42 | } else {
43 | EmptyView()
44 | }
45 | })
46 | .padding()
47 | }
48 |
49 | Spacer()
50 | }
51 |
52 | Button(action: { viewStore.send(.apply) }) {
53 | Text("Apply")
54 | .padding()
55 | }
56 | }
57 | .padding()
58 | }
59 | }
60 | }
61 |
62 | struct ShapeView_Previews: PreviewProvider {
63 | static var previews: some View {
64 | ShapeView(store: Store(
65 | initialState: ShapeState(type: .square),
66 | reducer: shapeReducer,
67 | environment: ()
68 | ))
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Shape/Tests/ShapeTests.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import Testing
3 | import XCTest
4 | @testable import Shape
5 |
6 | final class ShapeTests: XCTestCase {
7 | func testSlectShape() {
8 | let store = TestStore(
9 | initialState: ShapeState(type: .square),
10 | reducer: shapeReducer,
11 | environment: ()
12 | )
13 |
14 | store.assert(
15 | .send(.didSelectType(.circle)) {
16 | $0.type = .circle
17 | },
18 | .send(.didSelectType(.square)) {
19 | $0.type = .square
20 | },
21 | .send(.didSelectType(.circle)) {
22 | $0.type = .circle
23 | }
24 | )
25 | }
26 |
27 | func testApply() {
28 | let store = TestStore(
29 | initialState: ShapeState(type: .square),
30 | reducer: shapeReducer,
31 | environment: ()
32 | )
33 |
34 | store.assert(
35 | .send(.apply)
36 | )
37 | }
38 |
39 | func testPreviewSnapshot() {
40 | assertSnapshot(
41 | matching: ShapeView_Previews.previews,
42 | layout: .device(config: .iPhoneXr)
43 | )
44 | }
45 |
46 | func testSquareSnapshot() {
47 | assertSnapshot(
48 | matching: ShapeView(store: Store(
49 | initialState: ShapeState(type: .square),
50 | reducer: .empty,
51 | environment: ()
52 | )),
53 | layout: .device(config: .iPhoneXr)
54 | )
55 | }
56 |
57 | func testCircleSnapshot() {
58 | assertSnapshot(
59 | matching: ShapeView(store: Store(
60 | initialState: ShapeState(type: .circle),
61 | reducer: .empty,
62 | environment: ()
63 | )),
64 | layout: .device(config: .iPhoneXr)
65 | )
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Shape/Tests/__Snapshots__/ShapeTests/testCircleSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Shape/Tests/__Snapshots__/ShapeTests/testCircleSnapshot.dark.png
--------------------------------------------------------------------------------
/Shape/Tests/__Snapshots__/ShapeTests/testCircleSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Shape/Tests/__Snapshots__/ShapeTests/testCircleSnapshot.light.png
--------------------------------------------------------------------------------
/Shape/Tests/__Snapshots__/ShapeTests/testPreviewSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Shape/Tests/__Snapshots__/ShapeTests/testPreviewSnapshot.dark.png
--------------------------------------------------------------------------------
/Shape/Tests/__Snapshots__/ShapeTests/testPreviewSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Shape/Tests/__Snapshots__/ShapeTests/testPreviewSnapshot.light.png
--------------------------------------------------------------------------------
/Shape/Tests/__Snapshots__/ShapeTests/testSquareSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Shape/Tests/__Snapshots__/ShapeTests/testSquareSnapshot.dark.png
--------------------------------------------------------------------------------
/Shape/Tests/__Snapshots__/ShapeTests/testSquareSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Shape/Tests/__Snapshots__/ShapeTests/testSquareSnapshot.light.png
--------------------------------------------------------------------------------
/Tabs/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/Tabs/.swiftpm/xcode/xcshareddata/xcschemes/Tabs.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
54 |
55 |
61 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Tabs/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Tabs",
6 | platforms: [
7 | .iOS(.v14)
8 | ],
9 | products: [
10 | .library(
11 | name: "Tabs",
12 | targets: ["Tabs"]
13 | )
14 | ],
15 | dependencies: [
16 | .package(path: "../Common"),
17 | .package(path: "../Color"),
18 | .package(path: "../Shape"),
19 | .package(path: "../Preview"),
20 | .package(path: "../Testing")
21 | ],
22 | targets: [
23 | .target(
24 | name: "Tabs",
25 | dependencies: [
26 | "Common",
27 | "Color",
28 | "Shape",
29 | "Preview"
30 | ],
31 | path: "Sources"
32 | ),
33 | .testTarget(
34 | name: "TabsTests",
35 | dependencies: [
36 | "Tabs",
37 | "Testing"
38 | ],
39 | path: "Tests",
40 | exclude: ["__Snapshots__"]
41 | )
42 | ]
43 | )
44 |
--------------------------------------------------------------------------------
/Tabs/Sources/Tab.swift:
--------------------------------------------------------------------------------
1 | public enum Tab: Int, CaseIterable {
2 | case color
3 | case shape
4 | case preview
5 | }
6 |
7 | extension Tab: Identifiable {
8 | public var id: Int { rawValue }
9 | }
10 |
--------------------------------------------------------------------------------
/Tabs/Sources/TabsAction.swift:
--------------------------------------------------------------------------------
1 | import Color
2 | import Preview
3 | import Shape
4 |
5 | public enum TabsAction: Equatable {
6 | case didSelectTab(Tab)
7 | case color(ColorAction)
8 | case shape(ShapeAction)
9 | case preview(PreviewAction)
10 | }
11 |
--------------------------------------------------------------------------------
/Tabs/Sources/TabsReducer.swift:
--------------------------------------------------------------------------------
1 | import Color
2 | import ComposableArchitecture
3 | import Preview
4 | import Shape
5 |
6 | public let tabsReducer = Reducer.combine(
7 | colorReducer.pullback(
8 | state: \.color,
9 | action: /TabsAction.color,
10 | environment: { _ in () }
11 | ),
12 | shapeReducer.pullback(
13 | state: \.shape,
14 | action: /TabsAction.shape,
15 | environment: { _ in () }
16 | ),
17 | previewReducer.pullback(
18 | state: \.preview,
19 | action: /TabsAction.preview,
20 | environment: { _ in () }
21 | ),
22 | Reducer { state, action, _ in
23 | switch action {
24 | case let .didSelectTab(tab):
25 | state.selectedTab = tab
26 | return .none
27 |
28 | case .color(.apply):
29 | state.preview.color = state.color.rgb
30 | return .none
31 |
32 | case .shape(.apply):
33 | state.preview.shape = state.shape.type
34 | return .none
35 |
36 | case .color, .shape, .preview:
37 | return .none
38 | }
39 | }
40 | )
41 |
--------------------------------------------------------------------------------
/Tabs/Sources/TabsState.swift:
--------------------------------------------------------------------------------
1 | import Color
2 | import Preview
3 | import Shape
4 |
5 | public struct TabsState: Equatable {
6 | public init(
7 | selectedTab: Tab,
8 | color: ColorState,
9 | shape: ShapeState,
10 | preview: PreviewState
11 | ) {
12 | self.selectedTab = selectedTab
13 | self.color = color
14 | self.shape = shape
15 | self.preview = preview
16 | }
17 |
18 | public var selectedTab: Tab
19 | public var color: ColorState
20 | public var shape: ShapeState
21 | public var preview: PreviewState
22 | }
23 |
--------------------------------------------------------------------------------
/Tabs/Sources/TabsView.swift:
--------------------------------------------------------------------------------
1 | import Color
2 | import ComposableArchitecture
3 | import Preview
4 | import Shape
5 | import SwiftUI
6 |
7 | public struct TabsView: View {
8 | public init(store: Store) {
9 | self.store = store
10 | }
11 |
12 | let store: Store
13 |
14 | public var body: some View {
15 | WithViewStore(store) { viewStore in
16 | TabView(selection: viewStore.binding(
17 | get: \.selectedTab,
18 | send: TabsAction.didSelectTab
19 | )) {
20 | ForEach(Tab.allCases, content: tabView(_:))
21 | }
22 | }
23 | }
24 |
25 | private func tabView(_ tab: Tab) -> some View {
26 | view(for: tab)
27 | .tabItem {
28 | Image(systemName: image(for: tab))
29 | Text(title(for: tab))
30 | }
31 | .tag(tab)
32 | }
33 |
34 | private func title(for tab: Tab) -> String {
35 | switch tab {
36 | case .color: return "Color"
37 | case .shape: return "Shape"
38 | case .preview: return "Preview"
39 | }
40 | }
41 |
42 | private func image(for tab: Tab) -> String {
43 | switch tab {
44 | case .color: return "eyedropper.halffull"
45 | case .shape: return "square.on.circle"
46 | case .preview: return "eye"
47 | }
48 | }
49 |
50 | @ViewBuilder
51 | private func view(for tab: Tab) -> some View {
52 | switch tab {
53 | case .color:
54 | ColorView(store: store.scope(
55 | state: \.color,
56 | action: TabsAction.color
57 | ))
58 |
59 | case .shape:
60 | ShapeView(store: store.scope(
61 | state: \.shape,
62 | action: TabsAction.shape
63 | ))
64 |
65 | case .preview:
66 | PreviewView(store: store.scope(
67 | state: \.preview,
68 | action: TabsAction.preview
69 | ))
70 | }
71 | }
72 | }
73 |
74 | struct TabsView_Previews: PreviewProvider {
75 | static var previews: some View {
76 | TabsView(store: Store(
77 | initialState: TabsState(
78 | selectedTab: .preview,
79 | color: ColorState(rgb: RGBColor(0.5, 0.5, 0.5)),
80 | shape: ShapeState(type: .square),
81 | preview: PreviewState()
82 | ),
83 | reducer: tabsReducer,
84 | environment: ()
85 | ))
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Tabs/Tests/TabsTests.swift:
--------------------------------------------------------------------------------
1 | import Color
2 | import ComposableArchitecture
3 | import Preview
4 | import Shape
5 | import Testing
6 | import XCTest
7 | @testable import Tabs
8 |
9 | final class TabsTests: XCTestCase {
10 | func testSelectTab() {
11 | let store = TestStore(
12 | initialState: TabsState(
13 | selectedTab: .preview,
14 | color: ColorState(rgb: RGBColor(0, 0, 0)),
15 | shape: ShapeState(type: .circle),
16 | preview: PreviewState()
17 | ),
18 | reducer: tabsReducer,
19 | environment: ()
20 | )
21 |
22 | store.assert(
23 | .send(.didSelectTab(.color)) {
24 | $0.selectedTab = .color
25 | },
26 | .send(.didSelectTab(.shape)) {
27 | $0.selectedTab = .shape
28 | },
29 | .send(.didSelectTab(.preview)) {
30 | $0.selectedTab = .preview
31 | }
32 | )
33 | }
34 |
35 | func testApplyColor() {
36 | let store = TestStore(
37 | initialState: TabsState(
38 | selectedTab: .color,
39 | color: ColorState(rgb: RGBColor(0.5, 0.5, 0.5)),
40 | shape: ShapeState(type: .circle),
41 | preview: PreviewState()
42 | ),
43 | reducer: tabsReducer,
44 | environment: ()
45 | )
46 |
47 | store.assert(
48 | .send(.color(.apply)) {
49 | $0.preview.color = RGBColor(0.5, 0.5, 0.5)
50 | }
51 | )
52 | }
53 |
54 | func testApplyShape() {
55 | let store = TestStore(
56 | initialState: TabsState(
57 | selectedTab: .color,
58 | color: ColorState(rgb: RGBColor(0, 0, 0)),
59 | shape: ShapeState(type: .square),
60 | preview: PreviewState()
61 | ),
62 | reducer: tabsReducer,
63 | environment: ()
64 | )
65 |
66 | store.assert(
67 | .send(.shape(.apply)) {
68 | $0.preview.shape = .square
69 | }
70 | )
71 | }
72 |
73 | func testPreviewSnapshot() {
74 | assertSnapshot(
75 | matching: TabsView_Previews.previews,
76 | layout: .device(config: .iPhoneXr)
77 | )
78 | }
79 |
80 | func testColorTabSnapshot() {
81 | assertSnapshot(
82 | matching: TabsView(store: Store(
83 | initialState: TabsState(
84 | selectedTab: .color,
85 | color: ColorState(rgb: RGBColor(0.5, 0.5, 0.5)),
86 | shape: ShapeState(type: .circle),
87 | preview: PreviewState()
88 | ),
89 | reducer: .empty,
90 | environment: ()
91 | )),
92 | layout: .device(config: .iPhoneXr)
93 | )
94 | }
95 |
96 | func testShapeTabSnapshot() {
97 | assertSnapshot(
98 | matching: TabsView(store: Store(
99 | initialState: TabsState(
100 | selectedTab: .shape,
101 | color: ColorState(rgb: RGBColor(0.5, 0.5, 0.5)),
102 | shape: ShapeState(type: .circle),
103 | preview: PreviewState()
104 | ),
105 | reducer: .empty,
106 | environment: ()
107 | )),
108 | layout: .device(config: .iPhoneXr)
109 | )
110 | }
111 |
112 | func testPreviewTabSnapshot() {
113 | assertSnapshot(
114 | matching: TabsView(store: Store(
115 | initialState: TabsState(
116 | selectedTab: .preview,
117 | color: ColorState(rgb: RGBColor(0.5, 0.5, 0.5)),
118 | shape: ShapeState(type: .circle),
119 | preview: PreviewState()
120 | ),
121 | reducer: .empty,
122 | environment: ()
123 | )),
124 | layout: .device(config: .iPhoneXr)
125 | )
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/Tabs/Tests/__Snapshots__/TabsTests/testColorTabSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Tabs/Tests/__Snapshots__/TabsTests/testColorTabSnapshot.dark.png
--------------------------------------------------------------------------------
/Tabs/Tests/__Snapshots__/TabsTests/testColorTabSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Tabs/Tests/__Snapshots__/TabsTests/testColorTabSnapshot.light.png
--------------------------------------------------------------------------------
/Tabs/Tests/__Snapshots__/TabsTests/testPreviewSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Tabs/Tests/__Snapshots__/TabsTests/testPreviewSnapshot.dark.png
--------------------------------------------------------------------------------
/Tabs/Tests/__Snapshots__/TabsTests/testPreviewSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Tabs/Tests/__Snapshots__/TabsTests/testPreviewSnapshot.light.png
--------------------------------------------------------------------------------
/Tabs/Tests/__Snapshots__/TabsTests/testPreviewTabSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Tabs/Tests/__Snapshots__/TabsTests/testPreviewTabSnapshot.dark.png
--------------------------------------------------------------------------------
/Tabs/Tests/__Snapshots__/TabsTests/testPreviewTabSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Tabs/Tests/__Snapshots__/TabsTests/testPreviewTabSnapshot.light.png
--------------------------------------------------------------------------------
/Tabs/Tests/__Snapshots__/TabsTests/testShapeTabSnapshot.dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Tabs/Tests/__Snapshots__/TabsTests/testShapeTabSnapshot.dark.png
--------------------------------------------------------------------------------
/Tabs/Tests/__Snapshots__/TabsTests/testShapeTabSnapshot.light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrarski/swift-composable-app-example/7811d370992c204d618c66ce61d1ad4e1eee8f7e/Tabs/Tests/__Snapshots__/TabsTests/testShapeTabSnapshot.light.png
--------------------------------------------------------------------------------
/Testing/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/Testing/.swiftpm/xcode/xcshareddata/xcschemes/Testing.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Testing/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Testing",
6 | platforms: [
7 | .iOS(.v14)
8 | ],
9 | products: [
10 | .library(
11 | name: "Testing",
12 | targets: ["Testing"]
13 | )
14 | ],
15 | dependencies: [
16 | .package(
17 | name: "SnapshotTesting",
18 | url: "https://github.com/pointfreeco/swift-snapshot-testing.git",
19 | from: "1.8.2"
20 | )
21 | ],
22 | targets: [
23 | .target(
24 | name: "Testing",
25 | dependencies: ["SnapshotTesting"],
26 | path: "Sources"
27 | )
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/Testing/Sources/SnapshotTesting.swift:
--------------------------------------------------------------------------------
1 | import SnapshotTesting
2 | import SwiftUI
3 | import UIKit
4 |
5 | public var isRecording: Bool {
6 | get { SnapshotTesting.isRecording }
7 | set { SnapshotTesting.isRecording = newValue }
8 | }
9 |
10 | public func assertSnapshot(
11 | matching value: Value,
12 | drawHierarchyInKeyWindow: Bool = false,
13 | precision: Float = 1,
14 | layout: SwiftUISnapshotLayout = .sizeThatFits,
15 | userInterfaceStyles: [UIUserInterfaceStyle] = [.light, .dark],
16 | named name: String? = nil,
17 | record recording: Bool = false,
18 | file: StaticString = #file,
19 | testName: String = #function,
20 | line: UInt = #line
21 | ) {
22 | userInterfaceStyles.forEach { userInterfaceStyle in
23 | SnapshotTesting.assertSnapshot(
24 | matching: value,
25 | as: .image(
26 | drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
27 | precision: precision,
28 | layout: layout,
29 | traits: .init(userInterfaceStyle: userInterfaceStyle)
30 | ),
31 | named: [name, userInterfaceStyle.name]
32 | .compactMap { $0 }
33 | .joined(separator: "-"),
34 | record: recording,
35 | file: file,
36 | testName: testName,
37 | line: line
38 | )
39 | }
40 | }
41 |
42 | private extension UIUserInterfaceStyle {
43 | var name: String {
44 | switch self {
45 | case .dark: return "dark"
46 | case .light: return "light"
47 | case .unspecified: fatalError()
48 | @unknown default: fatalError()
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------