├── Sources
├── ComposableArchitecture
│ ├── Internal
│ │ ├── Exports.swift
│ │ ├── Locking.swift
│ │ ├── Throttling.swift
│ │ ├── Diff.swift
│ │ └── Debug.swift
│ ├── Effects
│ │ ├── Debouncing.swift
│ │ ├── Timer.swift
│ │ └── Cancellation.swift
│ ├── UIKit
│ │ ├── Alert+UIKit.swift
│ │ └── IfLetUIKit.swift
│ ├── ViewStore.swift
│ ├── Debugging
│ │ ├── ReducerInstrumentation.swift
│ │ └── ReducerDebugging.swift
│ ├── Helpers
│ │ └── Alert.swift
│ ├── Store.swift
│ └── Effect.swift
└── ComposableCoreLocation
│ ├── Internal
│ └── Exports.swift
│ ├── Models
│ ├── Region.swift
│ ├── Beacon.swift
│ ├── Visit.swift
│ ├── Heading.swift
│ └── Location.swift
│ ├── Live.swift
│ └── Mock.swift
├── .gitignore
├── Examples
└── CaseStudies
│ ├── UIKitCaseStudies
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ ├── AppIcon.png
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── Internal
│ │ ├── ActivityIndicatorViewController.swift
│ │ └── IfLetStoreController.swift
│ ├── SceneDelegate.swift
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── Info.plist
│ ├── ListsOfState.swift
│ ├── CounterViewController.swift
│ ├── RootViewController.swift
│ ├── NavigateAndLoad.swift
│ └── LoadThenNavigate.swift
│ ├── README.md
│ ├── UIKitCaseStudiesTests
│ ├── UIKitCaseStudiesTests.swift
│ └── Info.plist
│ └── CaseStudies.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ └── CaseStudies (UIKit).xcscheme
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ ├── ComposableCoreLocation.xcscheme
│ ├── ComposableArchitecture.xcscheme
│ └── rxswift-composable-architecture-Package.xcscheme
├── ComposableArchitecture.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── .github
├── ISSUE_TEMPLATE
│ ├── question.md
│ └── bug_report.md
├── workflows
│ ├── ci.yml
│ └── format.yml
└── CODE_OF_CONDUCT.md
├── Makefile
├── Tests
├── ComposableArchitectureTests
│ ├── Helpers
│ │ └── TestScheduler.swift
│ ├── MemoryManagementTests.swift
│ ├── EffectDebounceTests.swift
│ ├── TimerTests.swift
│ ├── Internal
│ │ └── EffectThrottleTests.swift
│ ├── ComposableArchitectureTests.swift
│ ├── EffectTests.swift
│ ├── ReducerTests.swift
│ ├── EffectCancellationTests.swift
│ └── StoreTests.swift
└── ComposableCoreLocationTests
│ └── ComposableCoreLocationTests.swift
├── LICENSE
├── Package.swift
└── ComposableArchitecture.podspec
/Sources/ComposableArchitecture/Internal/Exports.swift:
--------------------------------------------------------------------------------
1 | @_exported import CasePaths
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | Package.resolved
7 |
--------------------------------------------------------------------------------
/Sources/ComposableCoreLocation/Internal/Exports.swift:
--------------------------------------------------------------------------------
1 | @_exported import ComposableArchitecture
2 | @_exported import CoreLocation
3 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudies/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/README.md:
--------------------------------------------------------------------------------
1 | # Composable Architecture Case Studies
2 |
3 | This project includes a number of digestible examples of how to solve common problems using the Composable Architecture.
4 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudiesTests/UIKitCaseStudiesTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import UIKitCaseStudies
4 |
5 | class UIKitCaseStudiesTests: XCTestCase {
6 | func testExample() {
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dannyhertz/rxswift-composable-architecture/HEAD/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png
--------------------------------------------------------------------------------
/Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ComposableArchitecture.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ComposableArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Have a question about the Composable Architecture?
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | The Composable Architecture uses GitHub issues for bugs. For more general discussion and help, please use [the Swift forum](https://forums.swift.org/c/related-projects/swift-composable-architecture) first.
11 |
--------------------------------------------------------------------------------
/Sources/ComposableArchitecture/Internal/Locking.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension UnsafeMutablePointer where Pointee == os_unfair_lock_s {
4 | @inlinable @discardableResult
5 | func sync(_ work: () -> R) -> R {
6 | os_unfair_lock_lock(self)
7 | defer { os_unfair_lock_unlock(self) }
8 | return work()
9 | }
10 | }
11 |
12 | extension NSRecursiveLock {
13 | @inlinable @discardableResult
14 | func sync(work: () -> R) -> R {
15 | self.lock()
16 | defer { self.unlock() }
17 | return work()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - '*'
10 |
11 | jobs:
12 | library:
13 | runs-on: macOS-latest
14 | strategy:
15 | matrix:
16 | xcode:
17 | - 11.4
18 | - 11.5
19 | - 11.6
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Select Xcode ${{ matrix.xcode }}
23 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
24 | - name: Run tests
25 | run: make test
26 |
--------------------------------------------------------------------------------
/.github/workflows/format.yml:
--------------------------------------------------------------------------------
1 | name: Format
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | swift_format:
10 | name: swift-format
11 | runs-on: macOS-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Install
15 | run: brew install swift-format
16 | - name: Format
17 | run: make format
18 | - uses: stefanzweifel/git-auto-commit-action@v4
19 | with:
20 | commit_message: Run swift-format
21 | branch: 'master'
22 | env:
23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PLATFORM_IOS = iOS Simulator,name=iPhone 11 Pro Max
2 | PLATFORM_MACOS = macOS
3 | PLATFORM_TVOS = tvOS Simulator,name=Apple TV 4K (at 1080p)
4 |
5 | default: test
6 |
7 | test:
8 | instruments -s devices
9 | xcodebuild test \
10 | -scheme ComposableArchitecture \
11 | -destination platform="$(PLATFORM_IOS)"
12 | xcodebuild test \
13 | -scheme ComposableArchitecture \
14 | -destination platform="$(PLATFORM_MACOS)"
15 | xcodebuild test \
16 | -scheme ComposableArchitecture \
17 | -destination platform="$(PLATFORM_TVOS)"
18 | xcodebuild test \
19 | -scheme "CaseStudies (UIKit)" \
20 | -destination platform="$(PLATFORM_IOS)"
21 |
22 | format:
23 | swift format --in-place --recursive ./Package.swift ./Sources ./Tests
24 |
25 | .PHONY: format test
--------------------------------------------------------------------------------
/Tests/ComposableArchitectureTests/Helpers/TestScheduler.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import RxSwift
3 | import RxTest
4 |
5 | private let testSchedulerResultion = 0.01
6 |
7 | extension TestScheduler {
8 |
9 | public static func `default`(withInitialClock initialClock: Int = 0) -> TestScheduler {
10 | TestScheduler(
11 | initialClock: initialClock, resolution: testSchedulerResultion, simulateProcessingDelay: false
12 | )
13 | }
14 |
15 | public func advance(by: TimeInterval = 0) {
16 | self.advanceTo(self.clock + Int(by * (1 / testSchedulerResultion)))
17 | }
18 |
19 | public func tick() {
20 | self.advanceTo(self.clock + 1)
21 | }
22 |
23 | public func run() {
24 | self.advanceTo(Int(Date.distantFuture.timeIntervalSince1970))
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudies/Internal/ActivityIndicatorViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | final class ActivityIndicatorViewController: UIViewController {
4 | override func viewDidLoad() {
5 | super.viewDidLoad()
6 |
7 | self.view.backgroundColor = .white
8 |
9 | let activityIndicator = UIActivityIndicatorView()
10 | activityIndicator.startAnimating()
11 | activityIndicator.translatesAutoresizingMaskIntoConstraints = false
12 | self.view.addSubview(activityIndicator)
13 |
14 | NSLayoutConstraint.activate([
15 | activityIndicator.centerXAnchor.constraint(
16 | equalTo: self.view.safeAreaLayoutGuide.centerXAnchor),
17 | activityIndicator.centerYAnchor.constraint(
18 | equalTo: self.view.safeAreaLayoutGuide.centerYAnchor),
19 | ])
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | Give a clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Zip up a project that reproduces the behavior and attach it by dragging it here.
15 |
16 | ```swift
17 | // And/or enter code that reproduces the behavior here.
18 |
19 | ```
20 |
21 | **Expected behavior**
22 | Give a clear and concise description of what you expected to happen.
23 |
24 | **Screenshots**
25 | If applicable, add screenshots to help explain your problem.
26 |
27 | **Environment**
28 | - Xcode [e.g. 11.4.1]
29 | - Swift [e.g. 5.2.2]
30 | - OS (if applicable): [e.g. iOS 13]
31 |
32 | **Additional context**
33 | Add any more context about the problem here.
34 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudiesTests/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 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudies/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
4 | var window: UIWindow?
5 |
6 | func scene(
7 | _ scene: UIScene,
8 | willConnectTo session: UISceneSession,
9 | options connectionOptions: UIScene.ConnectionOptions
10 | ) {
11 | self.window = (scene as? UIWindowScene).map(UIWindow.init(windowScene:))
12 | self.window?.rootViewController = UINavigationController(
13 | rootViewController: RootViewController())
14 | self.window?.makeKeyAndVisible()
15 | }
16 | }
17 |
18 | @UIApplicationMain
19 | class AppDelegate: UIResponder, UIApplicationDelegate {
20 | func application(
21 | _ application: UIApplication,
22 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
23 | ) -> Bool {
24 | true
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Point-Free, Inc.
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 |
--------------------------------------------------------------------------------
/Sources/ComposableCoreLocation/Models/Region.swift:
--------------------------------------------------------------------------------
1 | import CoreLocation
2 |
3 | /// A value type wrapper for `CLRegion`. This type is necessary so that we can do equality checks
4 | /// and write tests against its values.
5 | public struct Region: Hashable {
6 | public let rawValue: CLRegion?
7 |
8 | public var identifier: String
9 | public var notifyOnEntry: Bool
10 | public var notifyOnExit: Bool
11 |
12 | init(rawValue: CLRegion) {
13 | self.rawValue = rawValue
14 |
15 | self.identifier = rawValue.identifier
16 | self.notifyOnEntry = rawValue.notifyOnEntry
17 | self.notifyOnExit = rawValue.notifyOnExit
18 | }
19 |
20 | init(
21 | identifier: String,
22 | notifyOnEntry: Bool,
23 | notifyOnExit: Bool
24 | ) {
25 | self.rawValue = nil
26 |
27 | self.identifier = identifier
28 | self.notifyOnEntry = notifyOnEntry
29 | self.notifyOnExit = notifyOnExit
30 | }
31 |
32 | public static func == (lhs: Self, rhs: Self) -> Bool {
33 | lhs.identifier == rhs.identifier
34 | && lhs.notifyOnEntry == rhs.notifyOnEntry
35 | && lhs.notifyOnExit == rhs.notifyOnExit
36 | }
37 |
38 | public func hash(into hasher: inout Hasher) {
39 | hasher.combine(self.identifier)
40 | hasher.combine(self.notifyOnExit)
41 | hasher.combine(self.notifyOnEntry)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "rxswift-composable-architecture",
7 | platforms: [
8 | .iOS(.v12),
9 | .macOS(.v10_15),
10 | .tvOS(.v13),
11 | .watchOS(.v6),
12 | ],
13 | products: [
14 | .library(
15 | name: "ComposableArchitecture",
16 | targets: ["ComposableArchitecture"]
17 | ),
18 | .library(
19 | name: "ComposableCoreLocation",
20 | targets: ["ComposableCoreLocation"]
21 | ),
22 | ],
23 | dependencies: [
24 | .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "0.1.1"),
25 | .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "5.0.0"),
26 | ],
27 | targets: [
28 | .target(
29 | name: "ComposableArchitecture",
30 | dependencies: [
31 | "CasePaths", "RxSwift", "RxRelay",
32 | ]
33 | ),
34 | .testTarget(
35 | name: "ComposableArchitectureTests",
36 | dependencies: [
37 | "ComposableArchitecture", "RxTest",
38 | ]
39 | ),
40 | .target(
41 | name: "ComposableCoreLocation",
42 | dependencies: [
43 | "ComposableArchitecture"
44 | ]
45 | ),
46 | .testTarget(
47 | name: "ComposableCoreLocationTests",
48 | dependencies: [
49 | "ComposableCoreLocation"
50 | ]
51 | ),
52 | ]
53 | )
54 |
--------------------------------------------------------------------------------
/Tests/ComposableArchitectureTests/MemoryManagementTests.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import RxSwift
3 | import RxTest
4 | import XCTest
5 |
6 | final class MemoryManagementTests: XCTestCase {
7 | var disposeBag = DisposeBag()
8 |
9 | func testOwnership_ScopeHoldsOntoParent() {
10 | let counterReducer = Reducer { state, _, _ in
11 | state += 1
12 | return .none
13 | }
14 | let store = Store(initialState: 0, reducer: counterReducer, environment: ())
15 | .scope(state: { "\($0)" })
16 | .scope(state: { Int($0)! })
17 | let viewStore = ViewStore(store)
18 |
19 | var count = 0
20 | viewStore.publisher
21 | .subscribe(onNext: { count = $0 })
22 | .disposed(by: disposeBag)
23 |
24 | XCTAssertEqual(count, 0)
25 | viewStore.send(())
26 | XCTAssertEqual(count, 1)
27 | }
28 |
29 | func testOwnership_ViewStoreHoldsOntoStore() {
30 | let counterReducer = Reducer { state, _, _ in
31 | state += 1
32 | return .none
33 | }
34 | let viewStore = ViewStore(Store(initialState: 0, reducer: counterReducer, environment: ()))
35 |
36 | var count = 0
37 | viewStore.publisher
38 | .subscribe(onNext: { count = $0 })
39 | .disposed(by: disposeBag)
40 |
41 | XCTAssertEqual(count, 0)
42 | viewStore.send(())
43 | XCTAssertEqual(count, 1)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ComposableArchitecture.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'ComposableArchitecture'
3 | s.version = '0.8.0'
4 | s.summary = 'A RxSwift fork of The Composable Architecture.'
5 |
6 | s.description = <<-DESC
7 | Point-Free’s The Composable Architecture uses Apple's Combine framework as the basis of its Effect type. Unfortunately, Combine is only available on iOS 13 and macOS 10.15 and above. In order to be able to use it with earlier versions of the OSes, this fork has adapted The Composable Architecture to use RxSwift as the basis for the Effect type. Much of this work was also inspired by the wonderful ReactiveSwift port of this project as well.
8 | DESC
9 |
10 | s.homepage = 'https://github.com/dannyhertz/rxswift-composable-architecture'
11 | s.author = { 'Danny Hertz' => 'me@dannyhertz.com' }
12 | s.source = { :git => 'https://github.com/dannyhertz/rxswift-composable-architecture.git', :tag => s.version.to_s }
13 | s.license = { :type => 'MIT' }
14 |
15 | s.ios.deployment_target = '12.0'
16 | s.swift_version = '5.2'
17 |
18 | s.source_files = 'Sources/ComposableArchitecture/**/*.swift'
19 |
20 | s.dependency 'CasePaths'
21 | s.dependency 'Overture'
22 | s.dependency 'RxSwift', '~> 5.1.1'
23 | s.dependency 'RxRelay'
24 | s.xcconfig = { 'ENABLE_BITCODE' => 'NO' }
25 | end
26 |
27 |
--------------------------------------------------------------------------------
/Sources/ComposableCoreLocation/Models/Beacon.swift:
--------------------------------------------------------------------------------
1 | import CoreLocation
2 |
3 | /// A value type wrapper for `CLBeacon`. This type is necessary so that we can do equality checks
4 | /// and write tests against its values.
5 | @available(macOS, unavailable)
6 | @available(tvOS, unavailable)
7 | @available(watchOS, unavailable)
8 | public struct Beacon: Equatable {
9 | public let rawValue: CLBeacon?
10 |
11 | public var accuracy: CLLocationAccuracy
12 | public var major: NSNumber
13 | public var minor: NSNumber
14 | public var proximity: CLProximity
15 | public var rssi: Int
16 |
17 | public init(rawValue: CLBeacon) {
18 | self.rawValue = rawValue
19 |
20 | self.accuracy = rawValue.accuracy
21 | self.major = rawValue.major
22 | self.minor = rawValue.minor
23 | self.proximity = rawValue.proximity
24 | self.rssi = rawValue.rssi
25 | }
26 |
27 | init(
28 | accuracy: CLLocationAccuracy,
29 | major: NSNumber,
30 | minor: NSNumber,
31 | proximity: CLProximity,
32 | rssi: Int,
33 | timestamp: Date,
34 | uuid: UUID
35 | ) {
36 | self.rawValue = nil
37 | self.accuracy = accuracy
38 | self.major = major
39 | self.minor = minor
40 | self.proximity = proximity
41 | self.rssi = rssi
42 | }
43 |
44 | public static func == (lhs: Self, rhs: Self) -> Bool {
45 | return lhs.accuracy == rhs.accuracy
46 | && lhs.major == rhs.major
47 | && lhs.minor == rhs.minor
48 | && lhs.proximity == rhs.proximity
49 | && lhs.rssi == rhs.rssi
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/ComposableArchitecture/Effects/Debouncing.swift:
--------------------------------------------------------------------------------
1 | import Dispatch
2 | import RxSwift
3 |
4 | extension Effect {
5 | /// Turns an effect into one that can be debounced.
6 | ///
7 | /// To turn an effect into a debounce-able one you must provide an identifier, which is used to
8 | /// determine which in-flight effect should be canceled in order to start a new effect. Any
9 | /// hashable value can be used for the identifier, such as a string, but you can add a bit of
10 | /// protection against typos by defining a new type that conforms to `Hashable`, such as an empty
11 | /// struct:
12 | ///
13 | /// case let .textChanged(text):
14 | /// struct SearchId: Hashable {}
15 | ///
16 | /// return environment.search(text)
17 | /// .map(Action.searchResponse)
18 | /// .debounce(id: SearchId(), for: 0.5, scheduler: environment.mainQueue)
19 | ///
20 | /// - Parameters:
21 | /// - id: The effect's identifier.
22 | /// - dueTime: The duration you want to debounce for.
23 | /// - scheduler: The scheduler you want to deliver the debounced output to.
24 | /// - options: Scheduler options that customize the effect's delivery of elements.
25 | /// - Returns: An effect that publishes events only after a specified time elapses.
26 | public func debounce(
27 | id: AnyHashable,
28 | for dueTime: RxTimeInterval,
29 | scheduler: SchedulerType
30 | ) -> Effect {
31 | Observable.just(())
32 | .delay(dueTime, scheduler: scheduler)
33 | .flatMap { self }
34 | .eraseToEffect()
35 | .cancellable(id: id, cancelInFlight: true)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/ComposableCoreLocation/Models/Visit.swift:
--------------------------------------------------------------------------------
1 | import CoreLocation
2 |
3 | /// A value type wrapper for `CLVisit`. This type is necessary so that we can do equality checks
4 | /// and write tests against its values.
5 | @available(iOS 8, macCatalyst 13, *)
6 | @available(macOS, unavailable)
7 | @available(tvOS, unavailable)
8 | @available(watchOS, unavailable)
9 | public struct Visit: Equatable {
10 | public let rawValue: CLVisit?
11 |
12 | public var arrivalDate: Date
13 | public var coordinate: CLLocationCoordinate2D
14 | public var departureDate: Date
15 | public var horizontalAccuracy: CLLocationAccuracy
16 |
17 | init(visit: CLVisit) {
18 | self.rawValue = nil
19 |
20 | self.arrivalDate = visit.arrivalDate
21 | self.coordinate = visit.coordinate
22 | self.departureDate = visit.departureDate
23 | self.horizontalAccuracy = visit.horizontalAccuracy
24 | }
25 |
26 | public init(
27 | arrivalDate: Date,
28 | coordinate: CLLocationCoordinate2D,
29 | departureDate: Date,
30 | horizontalAccuracy: CLLocationAccuracy
31 | ) {
32 | self.rawValue = nil
33 |
34 | self.arrivalDate = arrivalDate
35 | self.coordinate = coordinate
36 | self.departureDate = departureDate
37 | self.horizontalAccuracy = horizontalAccuracy
38 | }
39 |
40 | public static func == (lhs: Self, rhs: Self) -> Bool {
41 | lhs.arrivalDate == rhs.arrivalDate
42 | && lhs.coordinate.latitude == rhs.coordinate.latitude
43 | && lhs.coordinate.longitude == rhs.coordinate.longitude
44 | && lhs.departureDate == rhs.departureDate
45 | && lhs.horizontalAccuracy == rhs.horizontalAccuracy
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/ComposableArchitecture/Effects/Timer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import RxSwift
3 |
4 | extension Effect where Output: RxAbstractInteger {
5 | /// Returns an effect that repeatedly emits the current time of the given
6 | /// scheduler on the given interval.
7 | ///
8 | /// This effect serves as a testable alternative to `Timer.publish`, which
9 | /// performs its work on a run loop, _not_ a scheduler.
10 | ///
11 | /// struct TimerId: Hashable {}
12 | ///
13 | /// switch action {
14 | /// case .startTimer:
15 | /// return Effect.timer(id: TimerId(), every: 1, on: environment.scheduler)
16 | /// .map { .timerUpdated($0) }
17 | /// case let .timerUpdated(date):
18 | /// state.date = date
19 | /// return .none
20 | /// case .stopTimer:
21 | /// return .cancel(id: TimerId())
22 | ///
23 | /// - Parameters:
24 | /// - interval: The time interval on which to publish events. For example, a value of `0.5`
25 | /// publishes an event approximately every half-second.
26 | /// - scheduler: The scheduler on which the timer runs.
27 | /// - tolerance: The allowed timing variance when emitting events. Defaults to `nil`, which
28 | /// allows any variance.
29 | /// - options: Scheduler options passed to the timer. Defaults to `nil`.
30 | public static func timer(
31 | id: AnyHashable,
32 | every interval: RxTimeInterval,
33 | on scheduler: SchedulerType
34 | ) -> Effect {
35 |
36 | return
37 | Observable
38 | .interval(interval, scheduler: scheduler)
39 | .eraseToEffect()
40 | .cancellable(id: id)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/ComposableArchitecture/UIKit/Alert+UIKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Danny Hertz on 7/15/20.
6 | //
7 |
8 | #if canImport(UIKit)
9 | import Foundation
10 | import UIKit
11 |
12 | extension AlertState.Button {
13 | func toUIKit(send: @escaping (Action) -> Void) -> UIAlertAction {
14 | let action = { if let action = self.action { send(action) } }
15 | switch self.type {
16 | case let .cancel(.some(label)):
17 | return .init(title: label, style: .cancel, handler: { _ in action() })
18 | case .cancel(.none):
19 | return .init(title: nil, style: .cancel, handler: { _ in action() })
20 | case let .default(label):
21 | return .init(title: label, style: .default, handler: { _ in action() })
22 | case let .destructive(label):
23 | return .init(title: label, style: .destructive, handler: { _ in action() })
24 | }
25 | }
26 | }
27 |
28 | extension AlertState {
29 | func toUIKit(send: @escaping (Action) -> Void) -> UIAlertController {
30 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
31 |
32 | if let primaryButton = primaryButton {
33 | alert.addAction(primaryButton.toUIKit(send: send))
34 | }
35 | if let secondaryButton = secondaryButton {
36 | alert.addAction(secondaryButton.toUIKit(send: send))
37 | }
38 |
39 | return alert
40 | }
41 | }
42 |
43 | extension UIAlertController {
44 | public static func alert(_ alert: AlertState, send: @escaping (Action) -> Void)
45 | -> UIAlertController
46 | {
47 | return alert.toUIKit(send: send)
48 | }
49 | }
50 | #endif
51 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudies/Internal/IfLetStoreController.swift:
--------------------------------------------------------------------------------
1 | import RxSwift
2 | import RxCocoa
3 | import ComposableArchitecture
4 | import UIKit
5 |
6 | final class IfLetStoreController: UIViewController {
7 | let store: Store
8 | let ifDestination: (Store) -> UIViewController
9 | let elseDestination: () -> UIViewController
10 |
11 | private var disposeBag = DisposeBag()
12 | private var viewController = UIViewController() {
13 | willSet {
14 | self.viewController.willMove(toParent: nil)
15 | self.viewController.view.removeFromSuperview()
16 | self.viewController.removeFromParent()
17 | self.addChild(newValue)
18 | self.view.addSubview(newValue.view)
19 | newValue.didMove(toParent: self)
20 | }
21 | }
22 |
23 | init(
24 | store: Store,
25 | then ifDestination: @escaping (Store) -> UIViewController,
26 | else elseDestination: @autoclosure @escaping () -> UIViewController
27 | ) {
28 | self.store = store
29 | self.ifDestination = ifDestination
30 | self.elseDestination = elseDestination
31 | super.init(nibName: nil, bundle: nil)
32 | }
33 |
34 | required init?(coder: NSCoder) {
35 | fatalError("init(coder:) has not been implemented")
36 | }
37 |
38 | override func viewDidLoad() {
39 | super.viewDidLoad()
40 |
41 | self.store.ifLet(
42 | then: { [weak self] store in
43 | guard let self = self else { return }
44 | self.viewController = self.ifDestination(store)
45 | },
46 | else: { [weak self] in
47 | guard let self = self else { return }
48 | self.viewController = self.elseDestination()
49 | }
50 | )
51 | .disposed(by: disposeBag)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudies/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Sources/ComposableCoreLocation/Models/Heading.swift:
--------------------------------------------------------------------------------
1 | import CoreLocation
2 |
3 | /// A value type wrapper for `CLHeading`. This type is necessary so that we can do equality checks
4 | /// and write tests against its values.
5 | @available(iOS 3, macCatalyst 13, watchOS 2, *)
6 | @available(macOS, unavailable)
7 | @available(tvOS, unavailable)
8 | public struct Heading: Equatable {
9 | public let rawValue: CLHeading?
10 |
11 | public var headingAccuracy: CLLocationDirection
12 | public var magneticHeading: CLLocationDirection
13 | public var timestamp: Date
14 | public var trueHeading: CLLocationDirection
15 | public var x: CLHeadingComponentValue
16 | public var y: CLHeadingComponentValue
17 | public var z: CLHeadingComponentValue
18 |
19 | init(rawValue: CLHeading) {
20 | self.rawValue = rawValue
21 |
22 | self.headingAccuracy = rawValue.headingAccuracy
23 | self.magneticHeading = rawValue.magneticHeading
24 | self.timestamp = rawValue.timestamp
25 | self.trueHeading = rawValue.trueHeading
26 | self.x = rawValue.x
27 | self.y = rawValue.y
28 | self.z = rawValue.z
29 | }
30 |
31 | init(
32 | headingAccuracy: CLLocationDirection,
33 | magneticHeading: CLLocationDirection,
34 | timestamp: Date,
35 | trueHeading: CLLocationDirection,
36 | x: CLHeadingComponentValue,
37 | y: CLHeadingComponentValue,
38 | z: CLHeadingComponentValue
39 | ) {
40 | self.rawValue = nil
41 |
42 | self.headingAccuracy = headingAccuracy
43 | self.magneticHeading = magneticHeading
44 | self.timestamp = timestamp
45 | self.trueHeading = trueHeading
46 | self.x = x
47 | self.y = y
48 | self.z = z
49 | }
50 |
51 | public static func == (lhs: Self, rhs: Self) -> Bool {
52 | lhs.headingAccuracy == rhs.headingAccuracy
53 | && lhs.magneticHeading == rhs.magneticHeading
54 | && lhs.timestamp == rhs.timestamp
55 | && lhs.trueHeading == rhs.trueHeading
56 | && lhs.x == rhs.x
57 | && lhs.y == rhs.y
58 | && lhs.z == rhs.z
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudies/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 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 |
37 |
38 |
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIRequiredDeviceCapabilities
43 |
44 | armv7
45 |
46 | UISupportedInterfaceOrientations
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationLandscapeLeft
50 | UIInterfaceOrientationLandscapeRight
51 |
52 | UISupportedInterfaceOrientations~ipad
53 |
54 | UIInterfaceOrientationPortrait
55 | UIInterfaceOrientationPortraitUpsideDown
56 | UIInterfaceOrientationLandscapeLeft
57 | UIInterfaceOrientationLandscapeRight
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "filename" : "AppIcon.png",
40 | "idiom" : "iphone",
41 | "scale" : "3x",
42 | "size" : "60x60"
43 | },
44 | {
45 | "idiom" : "ipad",
46 | "scale" : "1x",
47 | "size" : "20x20"
48 | },
49 | {
50 | "idiom" : "ipad",
51 | "scale" : "2x",
52 | "size" : "20x20"
53 | },
54 | {
55 | "idiom" : "ipad",
56 | "scale" : "1x",
57 | "size" : "29x29"
58 | },
59 | {
60 | "idiom" : "ipad",
61 | "scale" : "2x",
62 | "size" : "29x29"
63 | },
64 | {
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "40x40"
68 | },
69 | {
70 | "idiom" : "ipad",
71 | "scale" : "2x",
72 | "size" : "40x40"
73 | },
74 | {
75 | "idiom" : "ipad",
76 | "scale" : "1x",
77 | "size" : "76x76"
78 | },
79 | {
80 | "idiom" : "ipad",
81 | "scale" : "2x",
82 | "size" : "76x76"
83 | },
84 | {
85 | "idiom" : "ipad",
86 | "scale" : "2x",
87 | "size" : "83.5x83.5"
88 | },
89 | {
90 | "idiom" : "ios-marketing",
91 | "scale" : "1x",
92 | "size" : "1024x1024"
93 | }
94 | ],
95 | "info" : {
96 | "author" : "xcode",
97 | "version" : 1
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudies/ListsOfState.swift:
--------------------------------------------------------------------------------
1 | import RxSwift
2 | import ComposableArchitecture
3 | import UIKit
4 | import RxCocoa
5 |
6 | struct CounterListState: Equatable {
7 | var counters: [CounterState] = []
8 | }
9 |
10 | enum CounterListAction: Equatable {
11 | case counter(index: Int, action: CounterAction)
12 | }
13 |
14 | struct CounterListEnvironment {}
15 |
16 | let counterListReducer: Reducer =
17 | counterReducer.forEach(
18 | state: \CounterListState.counters,
19 | action: /CounterListAction.counter(index:action:),
20 | environment: { _ in CounterEnvironment() }
21 | )
22 |
23 | let cellIdentifier = "Cell"
24 |
25 | final class CountersTableViewController: UITableViewController {
26 | let store: Store
27 | let viewStore: ViewStore
28 | var disposeBag = DisposeBag()
29 |
30 | var dataSource: [CounterState] = [] {
31 | didSet { self.tableView.reloadData() }
32 | }
33 |
34 | init(store: Store) {
35 | self.store = store
36 | self.viewStore = ViewStore(store)
37 | super.init(nibName: nil, bundle: nil)
38 | }
39 |
40 | required init?(coder: NSCoder) {
41 | fatalError("init(coder:) has not been implemented")
42 | }
43 |
44 | override func viewDidLoad() {
45 | super.viewDidLoad()
46 |
47 | self.title = "Lists"
48 |
49 | self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)
50 |
51 | self.viewStore.publisher.counters
52 | .subscribe(onNext: { [weak self] in self?.dataSource = $0 })
53 | .disposed(by: disposeBag)
54 | }
55 |
56 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
57 | self.dataSource.count
58 | }
59 |
60 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
61 | -> UITableViewCell
62 | {
63 | let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
64 | cell.accessoryType = .disclosureIndicator
65 | cell.textLabel?.text = "\(self.dataSource[indexPath.row].count)"
66 | return cell
67 | }
68 |
69 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
70 | self.navigationController?.pushViewController(
71 | CounterViewController(
72 | store: self.store.scope(
73 | state: { $0.counters[indexPath.row] },
74 | action: { .counter(index: indexPath.row, action: $0) }
75 | )
76 | ),
77 | animated: true
78 | )
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Tests/ComposableArchitectureTests/EffectDebounceTests.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import RxSwift
3 | import RxTest
4 | import XCTest
5 |
6 | final class EffectDebounceTests: XCTestCase {
7 | var disposeBag = DisposeBag()
8 |
9 | func testDebounce() {
10 | let scheduler = TestScheduler.default()
11 | var values: [Int] = []
12 |
13 | func runDebouncedEffect(value: Int) {
14 | struct CancelToken: Hashable {}
15 | Observable.just(value)
16 | .eraseToEffect()
17 | .debounce(id: CancelToken(), for: .seconds(1), scheduler: scheduler)
18 | .subscribe(onNext: { values.append($0) })
19 | .disposed(by: disposeBag)
20 | }
21 |
22 | runDebouncedEffect(value: 1)
23 |
24 | // Nothing emits right away.
25 | XCTAssertEqual(values, [])
26 |
27 | // Waiting half the time also emits nothing
28 | scheduler.advance(by: 0.5)
29 | XCTAssertEqual(values, [])
30 |
31 | // Run another debounced effect.
32 | runDebouncedEffect(value: 2)
33 |
34 | // Waiting half the time emits nothing because the first debounced effect has been canceled.
35 | scheduler.advance(by: 0.5)
36 | XCTAssertEqual(values, [])
37 |
38 | // Run another debounced effect.
39 | runDebouncedEffect(value: 3)
40 |
41 | // Waiting half the time emits nothing because the second debounced effect has been canceled.
42 | scheduler.advance(by: 0.5)
43 | XCTAssertEqual(values, [])
44 |
45 | // Waiting the rest of the time emits the final effect value.
46 | scheduler.advance(by: 0.5)
47 | XCTAssertEqual(values, [3])
48 |
49 | // Running out the scheduler
50 | scheduler.run()
51 | XCTAssertEqual(values, [3])
52 | }
53 |
54 | func testDebounceIsLazy() {
55 | let scheduler = TestScheduler.default()
56 | var values: [Int] = []
57 | var effectRuns = 0
58 |
59 | func runDebouncedEffect(value: Int) {
60 | struct CancelToken: Hashable {}
61 |
62 | Observable.deferred { () -> Observable in
63 | effectRuns += 1
64 | return Observable.just(value)
65 | }
66 | .eraseToEffect()
67 | .debounce(id: CancelToken(), for: .seconds(1), scheduler: scheduler)
68 | .subscribe(onNext: { values.append($0) })
69 | .disposed(by: disposeBag)
70 | }
71 |
72 | runDebouncedEffect(value: 1)
73 |
74 | XCTAssertEqual(values, [])
75 | XCTAssertEqual(effectRuns, 0)
76 |
77 | scheduler.advance(by: 0.5)
78 |
79 | XCTAssertEqual(values, [])
80 | XCTAssertEqual(effectRuns, 0)
81 |
82 | scheduler.advance(by: 0.5)
83 |
84 | XCTAssertEqual(values, [1])
85 | XCTAssertEqual(effectRuns, 1)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/ComposableCoreLocation.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 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudies/CounterViewController.swift:
--------------------------------------------------------------------------------
1 | import RxSwift
2 | import ComposableArchitecture
3 | import UIKit
4 | import RxCocoa
5 |
6 | struct CounterState: Equatable {
7 | var count = 0
8 | }
9 |
10 | enum CounterAction: Equatable {
11 | case decrementButtonTapped
12 | case incrementButtonTapped
13 | }
14 |
15 | struct CounterEnvironment {}
16 |
17 | let counterReducer = Reducer { state, action, _ in
18 | switch action {
19 | case .decrementButtonTapped:
20 | state.count -= 1
21 | return .none
22 | case .incrementButtonTapped:
23 | state.count += 1
24 | return .none
25 | }
26 | }
27 |
28 | final class CounterViewController: UIViewController {
29 | let viewStore: ViewStore
30 | var disposeBag = DisposeBag()
31 |
32 | init(store: Store) {
33 | self.viewStore = ViewStore(store)
34 | super.init(nibName: nil, bundle: nil)
35 | }
36 |
37 | required init?(coder: NSCoder) {
38 | fatalError("init(coder:) has not been implemented")
39 | }
40 |
41 | override func viewDidLoad() {
42 | super.viewDidLoad()
43 |
44 | self.view.backgroundColor = .white
45 |
46 | let decrementButton = UIButton(type: .system)
47 | decrementButton.addTarget(self, action: #selector(decrementButtonTapped), for: .touchUpInside)
48 | decrementButton.setTitle("−", for: .normal)
49 |
50 | let countLabel = UILabel()
51 | countLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular)
52 |
53 | let incrementButton = UIButton(type: .system)
54 | incrementButton.addTarget(self, action: #selector(incrementButtonTapped), for: .touchUpInside)
55 | incrementButton.setTitle("+", for: .normal)
56 |
57 | let rootStackView = UIStackView(arrangedSubviews: [
58 | decrementButton,
59 | countLabel,
60 | incrementButton,
61 | ])
62 | rootStackView.translatesAutoresizingMaskIntoConstraints = false
63 | self.view.addSubview(rootStackView)
64 |
65 | NSLayoutConstraint.activate([
66 | rootStackView.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor),
67 | rootStackView.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor),
68 | ])
69 |
70 | self.viewStore.publisher
71 | .map { "\($0.count)" }
72 | .bind(to: countLabel.rx.text)
73 | .disposed(by: disposeBag)
74 |
75 | // .assign(to: \.text, on: countLabel)
76 | // .store(in: &self.cancellables)
77 | }
78 |
79 | @objc func decrementButtonTapped() {
80 | self.viewStore.send(.decrementButtonTapped)
81 | }
82 |
83 | @objc func incrementButtonTapped() {
84 | self.viewStore.send(.incrementButtonTapped)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Examples/CaseStudies/UIKitCaseStudies/RootViewController.swift:
--------------------------------------------------------------------------------
1 | import ComposableArchitecture
2 | import UIKit
3 | import RxSwift
4 |
5 | struct CaseStudy {
6 | let title: String
7 | let viewController: () -> UIViewController
8 |
9 | init(title: String, viewController: @autoclosure @escaping () -> UIViewController) {
10 | self.title = title
11 | self.viewController = viewController
12 | }
13 | }
14 |
15 | let dataSource: [CaseStudy] = [
16 | CaseStudy(
17 | title: "Basics",
18 | viewController: CounterViewController(
19 | store: Store(
20 | initialState: CounterState(),
21 | reducer: counterReducer,
22 | environment: CounterEnvironment()
23 | )
24 | )
25 | ),
26 | CaseStudy(
27 | title: "Lists",
28 | viewController: CountersTableViewController(
29 | store: Store(
30 | initialState: CounterListState(
31 | counters: [
32 | CounterState(),
33 | CounterState(),
34 | CounterState(),
35 | ]
36 | ),
37 | reducer: counterListReducer,
38 | environment: CounterListEnvironment()
39 | )
40 | )
41 | ),
42 | CaseStudy(
43 | title: "Navigate and load",
44 | viewController: EagerNavigationViewController(
45 | store: Store(
46 | initialState: EagerNavigationState(),
47 | reducer: eagerNavigationReducer,
48 | environment: EagerNavigationEnvironment(
49 | mainQueue: MainScheduler.instance
50 | )
51 | )
52 | )
53 | ),
54 | CaseStudy(
55 | title: "Load then navigate",
56 | viewController: LazyNavigationViewController(
57 | store: Store(
58 | initialState: LazyNavigationState(),
59 | reducer: lazyNavigationReducer,
60 | environment: LazyNavigationEnvironment(
61 | mainQueue: MainScheduler.instance
62 | )
63 | )
64 | )
65 | ),
66 | ]
67 |
68 | final class RootViewController: UITableViewController {
69 | override func viewDidLoad() {
70 | super.viewDidLoad()
71 |
72 | self.title = "Case Studies"
73 | self.navigationController?.navigationBar.prefersLargeTitles = true
74 | }
75 |
76 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
77 | dataSource.count
78 | }
79 |
80 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
81 | -> UITableViewCell
82 | {
83 | let caseStudy = dataSource[indexPath.row]
84 | let cell = UITableViewCell()
85 | cell.accessoryType = .disclosureIndicator
86 | cell.textLabel?.text = caseStudy.title
87 | return cell
88 | }
89 |
90 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
91 | let caseStudy = dataSource[indexPath.row]
92 | self.navigationController?.pushViewController(caseStudy.viewController(), animated: true)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Sources/ComposableArchitecture/Internal/Throttling.swift:
--------------------------------------------------------------------------------
1 | import Dispatch
2 | import Foundation
3 | import RxSwift
4 |
5 | extension Effect {
6 | /// Turns an effect into one that can be throttled.
7 | ///
8 | /// - Parameters:
9 | /// - id: The effect's identifier.
10 | /// - interval: The interval at which to find and emit the most recent element, expressed in
11 | /// the time system of the scheduler.
12 | /// - scheduler: The scheduler you want to deliver the throttled output to.
13 | /// - latest: A boolean value that indicates whether to publish the most recent element. If
14 | /// `false`, the publisher emits the first element received during the interval.
15 | /// - Returns: An effect that emits either the most-recent or first element received during the
16 | /// specified interval.
17 | func throttle(
18 | id: AnyHashable,
19 | for interval: RxTimeInterval,
20 | scheduler: SchedulerType,
21 | latest: Bool
22 | ) -> Effect {
23 | self.flatMap { value -> Observable