diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Bind.xcodeproj/project.pbxproj b/Bind.xcodeproj/project.pbxproj index 4d3b448..7b85810 100644 --- a/Bind.xcodeproj/project.pbxproj +++ b/Bind.xcodeproj/project.pbxproj @@ -78,6 +78,10 @@ 8755A7462347A93800C8AD07 /* RelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8755A73E2347A88D00C8AD07 /* RelayTests.swift */; }; 8755A7472347A93800C8AD07 /* RelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8755A73E2347A88D00C8AD07 /* RelayTests.swift */; }; 8755A7482347A93E00C8AD07 /* BindableTestsUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8755A73C23479B5700C8AD07 /* BindableTestsUIKit.swift */; }; + 8755A74A234B508900C8AD07 /* UnbindableMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8755A749234B508900C8AD07 /* UnbindableMock.swift */; }; + 8755A74B234B509000C8AD07 /* UnbindableMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8755A749234B508900C8AD07 /* UnbindableMock.swift */; }; + 8755A74C234B509000C8AD07 /* UnbindableMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8755A749234B508900C8AD07 /* UnbindableMock.swift */; }; + 8755A74D234B509000C8AD07 /* UnbindableMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8755A749234B508900C8AD07 /* UnbindableMock.swift */; }; 8774D75FE1168D2F1D4CE056 /* Bindable+UIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9B94AF56A71DFBCA50413B /* Bindable+UIImageView.swift */; }; 8916D29F0AEF2B864EED6B53 /* Bindable+UILabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8B8A3AE507973B94E0E2A1 /* Bindable+UILabel.swift */; }; 8A9566D706D206E76327E3DB /* Bindable+NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC8C0368846079A6AB4E3A8 /* Bindable+NSLayoutConstraint.swift */; }; @@ -233,6 +237,7 @@ 8755A73C23479B5700C8AD07 /* BindableTestsUIKit.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = BindableTestsUIKit.swift; sourceTree = ""; tabWidth = 4; }; 8755A73E2347A88D00C8AD07 /* RelayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayTests.swift; sourceTree = ""; }; 8755A7402347A92500C8AD07 /* SubscriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionTests.swift; sourceTree = ""; }; + 8755A749234B508900C8AD07 /* UnbindableMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnbindableMock.swift; sourceTree = ""; }; 87BCBA1C96D1499A2368C6C4 /* Bindable+UIViewController.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "Bindable+UIViewController.swift"; sourceTree = ""; tabWidth = 4; }; 8C9B94AF56A71DFBCA50413B /* Bindable+UIImageView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "Bindable+UIImageView.swift"; sourceTree = ""; tabWidth = 4; }; 91D93EE6FE99882FF6B5A4E3 /* BindTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BindTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -372,6 +377,7 @@ children = ( 643F9E946B7E3D2F8E2975BD /* PrinterMock.swift */, DBCF98045AC9F17CD5EF1563 /* BindableMock.swift */, + 8755A749234B508900C8AD07 /* UnbindableMock.swift */, ); path = Mock; sourceTree = ""; @@ -667,6 +673,7 @@ DEEB7E76CA6E133DCE59BD82 /* PrinterMock.swift in Sources */, 8755A73F2347A88D00C8AD07 /* RelayTests.swift in Sources */, 8755A7412347A92500C8AD07 /* SubscriptionTests.swift in Sources */, + 8755A74A234B508900C8AD07 /* UnbindableMock.swift in Sources */, 2D1C7E116D41AC6269445022 /* BindableMock.swift in Sources */, 637549C2D38B6A9E34E3C05E /* Output+Extension.swift in Sources */, 8755A73D23479B5700C8AD07 /* BindableTestsUIKit.swift in Sources */, @@ -741,6 +748,7 @@ 16B22C259512A6A4DB492648 /* BindableMock.swift in Sources */, F37FB2DC1E254424AC2BAE03 /* Output+Extension.swift in Sources */, 1DB68B189147E165DF6BB137 /* OutputTests.swift in Sources */, + 8755A74D234B509000C8AD07 /* UnbindableMock.swift in Sources */, 8755A7442347A93500C8AD07 /* SubscriptionTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -810,6 +818,7 @@ A8428486B13DF705F151FDD6 /* PrinterMock.swift in Sources */, 8755A7452347A93700C8AD07 /* RelayTests.swift in Sources */, 28AB432723BFCC656F66C488 /* BindableMock.swift in Sources */, + 8755A74B234B509000C8AD07 /* UnbindableMock.swift in Sources */, B603ED1DF83FB4467FB9B868 /* Output+Extension.swift in Sources */, 6F853E9E0052D86B01B6F4F3 /* OutputTests.swift in Sources */, 8755A7482347A93E00C8AD07 /* BindableTestsUIKit.swift in Sources */, @@ -826,6 +835,7 @@ 9D059CF3C7783B55F63C17CB /* BindableMock.swift in Sources */, 23ED5475B3FF87CBAF7B1701 /* Output+Extension.swift in Sources */, 42DCB57764A902476445A1ED /* OutputTests.swift in Sources */, + 8755A74C234B509000C8AD07 /* UnbindableMock.swift in Sources */, 8755A7432347A93400C8AD07 /* SubscriptionTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Package.swift b/Package.swift index b616a3b..80b359d 100644 --- a/Package.swift +++ b/Package.swift @@ -4,13 +4,18 @@ import PackageDescription let package = Package( - name: "Trigger", + name: "Bind", + platforms: [ + .iOS(.v11), + .macOS(.v10_12) + ], products: [ .library( - name: "Trigger", targets: ["Trigger"]) + name: "Bind", targets: ["Bind"]) ], targets: [ - .target( name: "Trigger"), - .testTarget( name: "TriggerTests", dependencies: ["Trigger"]) - ] + .target( name: "Bind"), + .testTarget( name: "BindTests", dependencies: ["Bind"]) + ], + swiftLanguageVersions: [.v5] ) diff --git a/Sources/Bind/Bindables/UIKit/Bindable+UIControl.swift b/Sources/Bind/Bindables/UIKit/Bindable+UIControl.swift index f57c0dd..df9ce43 100644 --- a/Sources/Bind/Bindables/UIKit/Bindable+UIControl.swift +++ b/Sources/Bind/Bindables/UIKit/Bindable+UIControl.swift @@ -2,7 +2,6 @@ import UIKit public extension Bindable where TargetType: UIControl { - var isSelected: Binder { return Binder(self.target) { control, isSelected in control.isSelected = isSelected diff --git a/Sources/Bind/Output.swift b/Sources/Bind/Output.swift index 3833709..40d6c41 100644 --- a/Sources/Bind/Output.swift +++ b/Sources/Bind/Output.swift @@ -187,6 +187,12 @@ public extension Output { return output } + /** + `filter` passes the Value through a predicate, if the functions true the `Value` is output, otherwise + it is filtered out. + - Parameter filter: The function that predicates on the `Value` to determine if it is output + - Returns: A new `Output` which predicates on the `Value` before outputting + */ func filter(_ filter: @escaping (Value) -> Bool) -> Output { let output = Output() @@ -198,14 +204,25 @@ public extension Output { return output } -} -// MARK: - Typed extensions -public extension Output where Value == Bool { - func invert() { - guard let currentValue = value else { - return + /** + Returns a new output that is the result of combining the outputted elements of the receiver + using the given closure. + - Parameter initial: The value to use as the initial accumulating value. + initialResult is passed to nextPartialResult the first time the closure is executed. + - Parameter nextPartialResult: + A closure that combines an accumulating value and the next value of the Output into a new accumulating + value that is then output to any binders. + - Returns: A new `Output` which acts as a tap of the combined accumulating values + */ + func reduce(initial: Result, nextPartialResult: @escaping (Result, Value) -> Result) -> Output { + let output = Output() + + bind { value in + let result = nextPartialResult(output.value ?? initial, value) + output.update(withValue: result) } - update(withValue: !currentValue) + + return output } } diff --git a/Sources/Bind/Subscription.swift b/Sources/Bind/Subscription.swift index 198b65f..a9c3f00 100644 --- a/Sources/Bind/Subscription.swift +++ b/Sources/Bind/Subscription.swift @@ -29,7 +29,7 @@ extension Subscription: Hashable { } public final class SubscriptionContainer { - private var container: [Subscription] = [] + var container: [Subscription] = [] public init() {} @@ -41,5 +41,7 @@ public final class SubscriptionContainer { for subscription in container { subscription.unsubscribe() } + + container = [] } } diff --git a/Tests/BindTests/BindableTestsUIKit.swift b/Tests/BindTests/BindableTestsUIKit.swift index 97e110a..480556f 100644 --- a/Tests/BindTests/BindableTestsUIKit.swift +++ b/Tests/BindTests/BindableTestsUIKit.swift @@ -7,6 +7,7 @@ import XCTest final class BindableTestsUIKit: XCTestCase { func testLabel() { let label = UILabel() + label.textColor = .black let attributedLabel = UILabel() let textOutput = Output() @@ -29,5 +30,97 @@ final class BindableTestsUIKit: XCTestCase { attributedTextOutput.update(withValue: NSAttributedString(string: "text")) XCTAssertEqual(attributedLabel.attributedText, NSAttributedString(string: "text")) } + + func testViewBools() { + let view = UIView() + + let hiddenOutput = Output() + let visibleOutput = Output() + let visibleAlpha = Output() + let visibleAlphaAnimated = Output() + let userInteractionEnabledOutput = Output() + let constraintsActive = Output() + + hiddenOutput.bind(to: view.binding.isHidden) + visibleOutput.bind(to: view.binding.isVisible) + visibleAlpha.bind(to: view.binding.isVisibleAlpha(animated: false)) + visibleAlphaAnimated.bind(to: view.binding.isVisibleAlpha(animated: true)) + userInteractionEnabledOutput.bind(to: view.binding.isUserInteractionEnabled) + constraintsActive.bind(to: view.binding.areConstraintsActive) + + hiddenOutput.update(withValue: true) + XCTAssertEqual(view.isHidden, true) + + visibleOutput.update(withValue: true) + XCTAssertEqual(view.isHidden, false) + + visibleAlpha.update(withValue: false) + XCTAssertEqual(view.alpha, 0) + + visibleAlpha.update(withValue: true) + XCTAssertEqual(view.alpha, 1) + + visibleAlphaAnimated.update(withValue: false) + XCTAssertEqual(view.alpha, 0) + + visibleAlphaAnimated.update(withValue: true) + XCTAssertEqual(view.alpha, 1) + + userInteractionEnabledOutput.update(withValue: false) + XCTAssertEqual(view.isUserInteractionEnabled, false) + + view.heightAnchor.constraint(equalToConstant: 10).isActive = true + XCTAssertEqual(view.constraints.count, 1) + constraintsActive.update(withValue: false) + XCTAssertEqual(view.constraints.count, 0) + } + + func testViewUIColors() { + let view = UIView() + + let backgroundColorOutput = Output() + let borderColorOutput = Output() + let tintColorOutput = Output() + + backgroundColorOutput.bind(to: view.binding.backgroundColor) + borderColorOutput.bind(to: view.binding.borderColor) + tintColorOutput.bind(to: view.binding.tintColor) + + backgroundColorOutput.update(withValue: .red) + XCTAssertEqual(view.backgroundColor, .red) + + borderColorOutput.update(withValue: .green) + XCTAssertEqual(view.layer.borderColor, UIColor.green.cgColor) + + tintColorOutput.update(withValue: .blue) + XCTAssertEqual(view.tintColor, .blue) + } + + func testViewCGFloat() { + let view = UIView() + + let borderWidthOutput = Output() + let cornerRadiusOutput = Output() + + borderWidthOutput.bind(to: view.binding.borderWidth) + cornerRadiusOutput.bind(to: view.binding.cornerRadius) + + borderWidthOutput.update(withValue: 30) + XCTAssertEqual(view.layer.borderWidth, 30) + + cornerRadiusOutput.update(withValue: 40) + XCTAssertEqual(view.layer.cornerRadius, 40) + } + + func testViewString() { + let view = UIView() + + let accessibilityOutput = Output() + + accessibilityOutput.bind(to: view.binding.accessibilityIdentifier) + + accessibilityOutput.update(withValue: "test-identifier") + XCTAssertEqual(view.accessibilityIdentifier, "test-identifier") + } } #endif diff --git a/Tests/BindTests/Mock/UnbindableMock.swift b/Tests/BindTests/Mock/UnbindableMock.swift new file mode 100644 index 0000000..5103cc8 --- /dev/null +++ b/Tests/BindTests/Mock/UnbindableMock.swift @@ -0,0 +1,10 @@ +@testable import Bind + +final class UnbindableMock: Unbindable { + var unbindCalledCount = 0 + var unbindSubscriptionArray = [Subscription]() + func unbind(for subscription: Subscription) { + unbindCalledCount += 1 + unbindSubscriptionArray.append(subscription) + } +} diff --git a/Tests/BindTests/OutputTests.swift b/Tests/BindTests/OutputTests.swift index 63619ab..9442484 100644 --- a/Tests/BindTests/OutputTests.swift +++ b/Tests/BindTests/OutputTests.swift @@ -49,23 +49,6 @@ final class OutputTests: XCTestCase { XCTAssertEqual(testObjectTwo.text, "Test") } - func testToggleWithValue() { - let output = Output(value: true) - XCTAssertEqual(output.latest, true) - - output.invert() - - XCTAssertEqual(output.latest, false) - } - - func testToggleWithNoValue() { - let output = Output() - XCTAssertNil(output.latest) - - output.invert() - XCTAssertNil(output.latest) - } - func testUnbind() { let testObject = BindableMock() @@ -221,7 +204,7 @@ final class OutputTests: XCTestCase { value.update(withValue: .one) XCTAssertEqual(mappedValue.latest, "one") - } + } func testFlatMap() { //swiftlint:disable:next nesting @@ -292,6 +275,40 @@ final class OutputTests: XCTestCase { XCTAssertEqual(merge.latest, 5) } + func testReduceReferenceType() { + class TestObject { //swiftlint:disable:this nesting + var currentString: String = "" + } + + let initial = Output() + + let reduced = initial + .reduce(initial: TestObject()) { current, number -> TestObject in + var currentString = current.currentString + currentString += "\(number)" + current.currentString = currentString + return current + } + + for value in [1, 2, 3, 4, 5] { + initial.update(withValue: value) + } + + XCTAssertEqual(reduced.latest?.currentString, "12345") + } + + func testReduceValueType() { + let initial = Output() + + let reduced = initial.reduce(initial: 0, nextPartialResult: +) + + for value in [1, 2, 3, 4, 5] { + initial.update(withValue: value) + } + + XCTAssertEqual(reduced.latest, 15) + } + func testDebug() { let printer = PrinterMock() let output1 = Output(printer: printer) diff --git a/Tests/BindTests/SubscriptionTests.swift b/Tests/BindTests/SubscriptionTests.swift index 228ec0f..d4eb0e5 100644 --- a/Tests/BindTests/SubscriptionTests.swift +++ b/Tests/BindTests/SubscriptionTests.swift @@ -2,5 +2,39 @@ import XCTest @testable import Bind final class SubscriptionTests: XCTestCase { + func testUnsubscribe() { + let mockUnbindable = UnbindableMock() + let subscription = Subscription(unbinder: mockUnbindable) + subscription.unsubscribe() + + XCTAssertEqual(mockUnbindable.unbindCalledCount, 1) + XCTAssertEqual(mockUnbindable.unbindSubscriptionArray.count, 1) + XCTAssertEqual(mockUnbindable.unbindSubscriptionArray.first, subscription) + } + + func testSubscriptionContainerAdd() { + let mockUnbindable = UnbindableMock() + let subscription = Subscription(unbinder: mockUnbindable) + let container = SubscriptionContainer() + + subscription.add(to: container) + + XCTAssertEqual(container.container.count, 1) + } + + func testSubscriptionContainerUnsubscribe() { + let mockUnbindable = UnbindableMock() + let subscription = Subscription(unbinder: mockUnbindable) + + let container = SubscriptionContainer() + subscription.add(to: container) + + XCTAssertEqual(container.container.count, 1) + + container.unsubscribe() + + XCTAssertEqual(container.container.count, 0) + XCTAssertEqual(mockUnbindable.unbindCalledCount, 1) + } }