diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11eda46..8ab81f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,27 +14,12 @@ jobs: strategy: matrix: xcode: - - 11.3 - 11.4 - 11.5 + - 11.6 steps: - uses: actions/checkout@v2 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: Run tests - run: make test-swift - - examples: - runs-on: macOS-latest - strategy: - matrix: - xcode: - - 11.3 - - 11.4 - - 11.5 - steps: - - uses: actions/checkout@v2 - - name: Select Xcode ${{ matrix.xcode }} - run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - - name: Run tests - run: make test-workspace + run: make test diff --git a/ComposableArchitecture.xcworkspace/contents.xcworkspacedata b/ComposableArchitecture.xcworkspace/contents.xcworkspacedata index ca3329e..8602eff 100644 --- a/ComposableArchitecture.xcworkspace/contents.xcworkspacedata +++ b/ComposableArchitecture.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj index a46146f..d48f609 100644 --- a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 7854FF92249318910094D8A8 /* UIKitCaseStudiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7854FF91249318910094D8A8 /* UIKitCaseStudiesTests.swift */; }; 78634FE924930375006E231F /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 78634FE824930375006E231F /* RxCocoa */; }; - 78B64BD42492F5EC00A47CE0 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 78B64BD32492F5EC00A47CE0 /* ComposableArchitecture */; }; + 78BC6CAC24D9BD200061AB2D /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 78BC6CAB24D9BD200061AB2D /* ComposableArchitecture */; }; DC25DC5F2450F13200082E81 /* IfLetStoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC25DC5E2450F13200082E81 /* IfLetStoreController.swift */; }; DC25DC612450F2B000082E81 /* LoadThenNavigate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC25DC602450F2B000082E81 /* LoadThenNavigate.swift */; }; DC25DC642450F2DF00082E81 /* ActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC25DC632450F2DF00082E81 /* ActivityIndicatorViewController.swift */; }; @@ -82,8 +82,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78BC6CAC24D9BD200061AB2D /* ComposableArchitecture in Frameworks */, 78634FE924930375006E231F /* RxCocoa in Frameworks */, - 78B64BD42492F5EC00A47CE0 /* ComposableArchitecture in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -187,8 +187,8 @@ ); name = UIKitCaseStudies; packageProductDependencies = ( - 78B64BD32492F5EC00A47CE0 /* ComposableArchitecture */, 78634FE824930375006E231F /* RxCocoa */, + 78BC6CAB24D9BD200061AB2D /* ComposableArchitecture */, ); productName = UIKitCaseStudies; productReference = DC4C6EA72450DD380066A05D /* UIKitCaseStudies.app */; @@ -245,8 +245,8 @@ ); mainGroup = DC89C40A24460F95006900B9; packageReferences = ( - 78B64BD22492F5EC00A47CE0 /* XCRemoteSwiftPackageReference "rx-swift-composable-architecture" */, 78634FE724930375006E231F /* XCRemoteSwiftPackageReference "RxSwift" */, + 78BC6CAA24D9BD200061AB2D /* XCRemoteSwiftPackageReference "rxswift-composable-architecture" */, ); productRefGroup = DC89C41424460F95006900B9 /* Products */; projectDirPath = ""; @@ -560,12 +560,12 @@ minimumVersion = 5.1.1; }; }; - 78B64BD22492F5EC00A47CE0 /* XCRemoteSwiftPackageReference "rx-swift-composable-architecture" */ = { + 78BC6CAA24D9BD200061AB2D /* XCRemoteSwiftPackageReference "rxswift-composable-architecture" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "git@github.com:dannyhertz/rx-swift-composable-architecture.git"; + repositoryURL = "https://github.com/dannyhertz/rxswift-composable-architecture.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.3.0; + minimumVersion = 0.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -576,9 +576,9 @@ package = 78634FE724930375006E231F /* XCRemoteSwiftPackageReference "RxSwift" */; productName = RxCocoa; }; - 78B64BD32492F5EC00A47CE0 /* ComposableArchitecture */ = { + 78BC6CAB24D9BD200061AB2D /* ComposableArchitecture */ = { isa = XCSwiftPackageProductDependency; - package = 78B64BD22492F5EC00A47CE0 /* XCRemoteSwiftPackageReference "rx-swift-composable-architecture" */; + package = 78BC6CAA24D9BD200061AB2D /* XCRemoteSwiftPackageReference "rxswift-composable-architecture" */; productName = ComposableArchitecture; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Makefile b/Makefile index b9e564c..b23928e 100644 --- a/Makefile +++ b/Makefile @@ -2,17 +2,10 @@ PLATFORM_IOS = iOS Simulator,name=iPhone 11 Pro Max PLATFORM_MACOS = macOS PLATFORM_TVOS = tvOS Simulator,name=Apple TV 4K (at 1080p) -default: test-all +default: test -test-all: test-swift test-workspace - -test-swift: - swift test \ - --enable-pubgrub-resolver \ - --enable-test-discovery \ - --parallel - -test-workspace: +test: + instruments -s devices xcodebuild test \ -scheme ComposableArchitecture \ -destination platform="$(PLATFORM_IOS)" @@ -22,8 +15,11 @@ test-workspace: xcodebuild test \ -scheme ComposableArchitecture \ -destination platform="$(PLATFORM_TVOS)" + xcodebuild test \ + -scheme "CaseStudies (UIKit)" \ + -destination platform="$(PLATFORM_IOS)" format: swift format --in-place --recursive ./Package.swift ./Sources ./Tests -.PHONY: format test-all test-swift test-workspace +.PHONY: format test \ No newline at end of file diff --git a/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift b/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift index 74e1dd8..7ba05ba 100644 --- a/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift +++ b/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift @@ -43,17 +43,27 @@ extension Store { then unwrap: @escaping (Store) -> Void, else: @escaping () -> Void ) -> Disposable where State == Wrapped? { - self.scope( - state: { state in - return - state - .distinctUntilChanged({ ($0 != nil) == ($1 != nil) }) - .do(onNext: { if $0 == nil { `else`() } }) - .compactMap { $0 } - }, - action: { $0 } - ) - .subscribe(onNext: unwrap) + + let elseDisposable = self + .scope( + state: { state in + state.distinctUntilChanged({ ($0 != nil) == ($1 != nil) }) + } + ) + .subscribe(onNext: { store in + if store.state == nil { `else`() } + }) + + let unwrapDisposable = self + .scope( + state: { state in + state.distinctUntilChanged({ ($0 != nil) == ($1 != nil) }) + .compactMap { $0 } + } + ) + .subscribe(onNext: unwrap) + + return CompositeDisposable(elseDisposable, unwrapDisposable) } /// An overload of `ifLet(then:else:)` for the times that you do not want to handle the `else` diff --git a/Tests/ComposableArchitectureTests/StoreTests.swift b/Tests/ComposableArchitectureTests/StoreTests.swift index 0be18c0..aeeecda 100644 --- a/Tests/ComposableArchitectureTests/StoreTests.swift +++ b/Tests/ComposableArchitectureTests/StoreTests.swift @@ -234,4 +234,120 @@ final class StoreTests: XCTestCase { store.send(.incr) XCTAssertEqual(ViewStore(store).state, 100_000) } + + func testPublisherScope() { + let appReducer = Reducer { state, action, _ in + state += action ? 1 : 0 + return .none + } + + let parentStore = Store(initialState: 0, reducer: appReducer, environment: ()) + + var outputs: [Int] = [] + + parentStore + .scope { $0.distinctUntilChanged() } + .subscribe(onNext: { + outputs.append($0.state) + }) + .disposed(by: disposeBag) + + XCTAssertEqual(outputs, [0]) + + parentStore.send(true) + XCTAssertEqual(outputs, [0, 1]) + + parentStore.send(false) + XCTAssertEqual(outputs, [0, 1]) + parentStore.send(false) + XCTAssertEqual(outputs, [0, 1]) + parentStore.send(false) + XCTAssertEqual(outputs, [0, 1]) + parentStore.send(false) + XCTAssertEqual(outputs, [0, 1]) + } + + func testIfLetAfterScope() { + struct AppState { + var count: Int? + } + + let appReducer = Reducer { state, action, _ in + state.count = action + return .none + } + + let parentStore = Store(initialState: AppState(), reducer: appReducer, environment: ()) + + // NB: This test needs to hold a strong reference to the emitted stores + var outputs: [Int?] = [] + var stores: [Any] = [] + + parentStore + .scope(state: \.count) + .ifLet( + then: { store in + stores.append(store) + outputs.append(store.state) + }, + else: { + outputs.append(nil) + }) + .disposed(by: disposeBag) + + XCTAssertEqual(outputs, [nil]) + + parentStore.send(1) + XCTAssertEqual(outputs, [nil, 1]) + + parentStore.send(nil) + XCTAssertEqual(outputs, [nil, 1, nil]) + + parentStore.send(1) + XCTAssertEqual(outputs, [nil, 1, nil, 1]) + + parentStore.send(nil) + XCTAssertEqual(outputs, [nil, 1, nil, 1, nil]) + + parentStore.send(1) + XCTAssertEqual(outputs, [nil, 1, nil, 1, nil, 1]) + + parentStore.send(nil) + XCTAssertEqual(outputs, [nil, 1, nil, 1, nil, 1, nil]) + } + + func testIfLetTwo() { + let parentStore = Store( + initialState: 0, + reducer: Reducer { state, action, _ in + if action { + state? += 1 + return .none + } else { + return Effect(value: true) + .observeOn(MainScheduler.instance) + .eraseToEffect() + } + }, + environment: () + ) + + parentStore.ifLet { childStore in + let vs = ViewStore(childStore) + + vs + .publisher + .subscribe(onNext: { _ in }) + .disposed(by: self.disposeBag) + + vs.send(false) + _ = XCTWaiter.wait(for: [.init()], timeout: 0.1) + vs.send(false) + _ = XCTWaiter.wait(for: [.init()], timeout: 0.1) + vs.send(false) + _ = XCTWaiter.wait(for: [.init()], timeout: 0.1) + XCTAssertEqual(vs.state, 3) + } + .disposed(by: disposeBag) + } }