diff --git a/.jazzy.yaml b/.jazzy.yaml index f7f62c3..75ce916 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -1,5 +1,5 @@ module: MaterialMotion -module_version: 1.1.0 +module_version: 1.2.0 sdk: iphonesimulator xcodebuild_arguments: - -workspace @@ -7,4 +7,4 @@ xcodebuild_arguments: - -scheme - MaterialMotion github_url: https://github.com/material-motion/material-motion-swift -github_file_prefix: https://github.com/material-motion/material-motion-swift/tree/v1.1.0 +github_file_prefix: https://github.com/material-motion/material-motion-swift/tree/v1.2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9923f79..0f7d64c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,53 @@ +# 1.2.0 + +This minor release introduces a new operator, `startWith`, which is meant to replace the `initialValue` operator. + +## New features + +### startWith operator + +The new `startWith` operator replaces `initialValue` and behaves slightly differently: `startWith` returns a [memory stream](https://github.com/staltz/xstream#memorystream), which is a stream that stores the last value it received and emits it upon subscription. What this means is that the provided initial value will only be emitted once once, ever, and that the resulting stream is guaranteed to emit a value on subscription. + +You can use startWith to turn a stream that may not initially emit values (like a gesture stream) and prime it with an initial value. For example, we use startWith in the "How to use reactive constraints" example in order to ensure that our axis line property is primed with a value. + + + +```swift +let axisCenterX = runtime.get(axisLine.layer).position.x() +runtime.add(Draggable(), to: exampleView) { $0 + .startWith(exampleView.layer.position) + .xLocked(to: axisCenterX) +} +runtime.add(Draggable(), to: axisLine) { $0.yLocked(to: axisLine.layer.position.y) } +``` + +## New deprecations + +- `initialValue(_:)` has been deprecated in favor of the new `startWith(_:)` operator. + +## Source changes + +* [Deprecate initialValue and provide startWith as a replacement.](https://github.com/material-motion/material-motion-swift/commit/2a5df59861c4ec0f737e2bc7e38d8bb0801e8e66) (Jeff Verkoeyen) +* [Renamed normalized.swift to normalizedBy.swift.](https://github.com/material-motion/material-motion-swift/commit/8ff079b6ba1322f3fae47a6f2eb2d72bc2158203) (Jeff Verkoeyen) +* [Rename unit test file rewriteTo.swift to rewriteToTests.swift.](https://github.com/material-motion/material-motion-swift/commit/0d81c70185865008282ee717e1bd7404227befe0) (Jeff Verkoeyen) + +## API changes + +Auto-generated by running: + + apidiff origin/stable release-candidate swift MaterialMotion.xcworkspace MaterialMotion + +## MotionObservableConvertible + +*new* method: `startWith(_:)` in `MotionObservableConvertible` + +*deprecated* method: `initialValue(_:)` in `MotionObservableConvertible`: Use `startWith(_:)` instead. + +## Non-source changes + +* [Fix README example.](https://github.com/material-motion/material-motion-swift/commit/21ab8263cc0f1bf0b0b7724954b55d9147c480d1) (Jeff Verkoeyen) +* [Fix typo.](https://github.com/material-motion/material-motion-swift/commit/16fbf643a4873c4bc79da83119ed735168070b60) (Jeff Verkoeyen) + # 1.1.0 This is our first minor release. It includes two new interactions and improvements to APIs for the common cases. diff --git a/MaterialMotion.podspec b/MaterialMotion.podspec index d4c168b..430ebc3 100644 --- a/MaterialMotion.podspec +++ b/MaterialMotion.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "MaterialMotion" s.summary = "Reactive motion driven by Core Animation." - s.version = "1.1.0" + s.version = "1.2.0" s.authors = "The Material Motion Authors" s.license = "Apache 2.0" s.homepage = "https://github.com/material-motion/material-motion-swift" diff --git a/Podfile.lock b/Podfile.lock index 9b6bb85..25acbd5 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -3,7 +3,7 @@ PODS: - IndefiniteObservable (3.1.0): - IndefiniteObservable/lib (= 3.1.0) - IndefiniteObservable/lib (3.1.0) - - MaterialMotion (1.1.0): + - MaterialMotion (1.2.0): - IndefiniteObservable (~> 3.0) DEPENDENCIES: @@ -17,7 +17,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: CatalogByConvention: be55c2263132e4f9f59299ac8a528ee8715b3275 IndefiniteObservable: 2789d61f487d8d37fa2b9c3153cc44d4447ff744 - MaterialMotion: 6ee4d44d39b074686d603c26c20a5816afdb50cd + MaterialMotion: 4a4f155a35fce5e1dad7cc838719ef1c9c590dc6 PODFILE CHECKSUM: f503265a0d60526a0d28c96dd4bdcfb40fb562fc diff --git a/README.md b/README.md index 2856328..4f91cda 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ runtime.add(arcMove, to: <#view#>)
ChangeDirection
runtime.add(ChangeDirection(withVelocityOf: gesture),
-            to: <#view#>)
+ to: <#direction#>) @@ -181,7 +181,7 @@ Makes use of: `Transition` and `Tween`. -A Material Design transition using assymetric transformations. +A Material Design transition using asymetric transformations. Makes use of: `Tween`. diff --git a/assets/constraints.gif b/assets/constraints.gif new file mode 100644 index 0000000..c0d42a2 Binary files /dev/null and b/assets/constraints.gif differ diff --git a/examples/HowToUseReactiveConstraintsExample.swift b/examples/HowToUseReactiveConstraintsExample.swift index 5e60a77..91f58f3 100644 --- a/examples/HowToUseReactiveConstraintsExample.swift +++ b/examples/HowToUseReactiveConstraintsExample.swift @@ -35,7 +35,7 @@ class HowToUseReactiveConstraintsExampleViewController: ExampleViewController { let axisCenterX = runtime.get(axisLine.layer).position.x() runtime.add(Draggable(), to: exampleView) { $0 - .initialValue(exampleView.layer.position) + .startWith(exampleView.layer.position) .xLocked(to: axisCenterX) } diff --git a/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj index 60fe369..1f859f2 100644 --- a/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj +++ b/examples/apps/Catalog/MaterialMotionCatalog.xcodeproj/project.pbxproj @@ -20,10 +20,10 @@ 660DA3211E7A106D008F7401 /* TweenExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660DA3201E7A106D008F7401 /* TweenExample.swift */; }; 6613A9DB1E832779004A3699 /* mergeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A9DA1E832779004A3699 /* mergeTests.swift */; }; 6613A9DD1E832913004A3699 /* rewriteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A9DC1E832913004A3699 /* rewriteTests.swift */; }; - 6613A9DF1E8329DF004A3699 /* rewriteTo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A9DE1E8329DF004A3699 /* rewriteTo.swift */; }; + 6613A9DF1E8329DF004A3699 /* rewriteToTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A9DE1E8329DF004A3699 /* rewriteToTests.swift */; }; 6613A9E11E832B18004A3699 /* rewriteRangeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A9E01E832B18004A3699 /* rewriteRangeTests.swift */; }; 6613A9E31E841461004A3699 /* anchorPointAdjustmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A9E21E841461004A3699 /* anchorPointAdjustmentTests.swift */; }; - 6613A9E71E84170B004A3699 /* normalizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A9E61E84170B004A3699 /* normalizeTests.swift */; }; + 6613A9E71E84170B004A3699 /* normalizedByTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A9E61E84170B004A3699 /* normalizedByTests.swift */; }; 6613A9E91E841856004A3699 /* offsetByTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A9E81E841856004A3699 /* offsetByTests.swift */; }; 6613A9EB1E84188D004A3699 /* scaledByTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A9EA1E84188D004A3699 /* scaledByTests.swift */; }; 6613A9ED1E841C04004A3699 /* rubberBandedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A9EC1E841C04004A3699 /* rubberBandedTests.swift */; }; @@ -63,7 +63,7 @@ 6695129B1E830AE500D8868D /* lowerBoundTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6695129A1E830AE500D8868D /* lowerBoundTests.swift */; }; 6695129D1E830B6E00D8868D /* upperBoundTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6695129C1E830B6E00D8868D /* upperBoundTests.swift */; }; 6695129F1E830C0800D8868D /* valveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6695129E1E830C0800D8868D /* valveTests.swift */; }; - 669512A11E830E1900D8868D /* initialValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669512A01E830E1900D8868D /* initialValueTests.swift */; }; + 669512A11E830E1900D8868D /* startWithTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669512A01E830E1900D8868D /* startWithTests.swift */; }; 66BD3CCB1E8046AD00AA413C /* MaterialExpansionExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BD3CCA1E8046AD00AA413C /* MaterialExpansionExample.swift */; }; 66DDFD0D1E71F0F100AA46B7 /* DraggableExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DDFD0C1E71F0F100AA46B7 /* DraggableExample.swift */; }; 66DDFD121E71F39700AA46B7 /* ExampleViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DDFD111E71F39700AA46B7 /* ExampleViews.swift */; }; @@ -102,10 +102,10 @@ 660DA3201E7A106D008F7401 /* TweenExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TweenExample.swift; path = ../../TweenExample.swift; sourceTree = ""; }; 6613A9DA1E832779004A3699 /* mergeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = mergeTests.swift; sourceTree = ""; }; 6613A9DC1E832913004A3699 /* rewriteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = rewriteTests.swift; sourceTree = ""; }; - 6613A9DE1E8329DF004A3699 /* rewriteTo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = rewriteTo.swift; sourceTree = ""; }; + 6613A9DE1E8329DF004A3699 /* rewriteToTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = rewriteToTests.swift; sourceTree = ""; }; 6613A9E01E832B18004A3699 /* rewriteRangeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = rewriteRangeTests.swift; sourceTree = ""; }; 6613A9E21E841461004A3699 /* anchorPointAdjustmentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = anchorPointAdjustmentTests.swift; sourceTree = ""; }; - 6613A9E61E84170B004A3699 /* normalizeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = normalizeTests.swift; sourceTree = ""; }; + 6613A9E61E84170B004A3699 /* normalizedByTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = normalizedByTests.swift; sourceTree = ""; }; 6613A9E81E841856004A3699 /* offsetByTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = offsetByTests.swift; sourceTree = ""; }; 6613A9EA1E84188D004A3699 /* scaledByTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = scaledByTests.swift; sourceTree = ""; }; 6613A9EC1E841C04004A3699 /* rubberBandedTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = rubberBandedTests.swift; sourceTree = ""; }; @@ -150,7 +150,7 @@ 6695129A1E830AE500D8868D /* lowerBoundTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = lowerBoundTests.swift; sourceTree = ""; }; 6695129C1E830B6E00D8868D /* upperBoundTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = upperBoundTests.swift; sourceTree = ""; }; 6695129E1E830C0800D8868D /* valveTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = valveTests.swift; sourceTree = ""; }; - 669512A01E830E1900D8868D /* initialValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = initialValueTests.swift; sourceTree = ""; }; + 669512A01E830E1900D8868D /* startWithTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = startWithTests.swift; sourceTree = ""; }; 66BD3CCA1E8046AD00AA413C /* MaterialExpansionExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MaterialExpansionExample.swift; path = ../../MaterialExpansionExample.swift; sourceTree = ""; }; 66DDFD0C1E71F0F100AA46B7 /* DraggableExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DraggableExample.swift; path = ../../DraggableExample.swift; sourceTree = ""; }; 66DDFD111E71F39700AA46B7 /* ExampleViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleViews.swift; sourceTree = ""; }; @@ -276,18 +276,18 @@ 669512921E8301D100D8868D /* dedupeTests.swift */, 669512961E8305AC00D8868D /* delayTests.swift */, 6605044D1E83146C009EDB8A /* distanceFromTests.swift */, - 669512A01E830E1900D8868D /* initialValueTests.swift */, 66F2C3C81E83245800DD9728 /* invertedTests.swift */, 6695129A1E830AE500D8868D /* lowerBoundTests.swift */, 6613A9DA1E832779004A3699 /* mergeTests.swift */, - 6613A9E61E84170B004A3699 /* normalizeTests.swift */, + 6613A9E61E84170B004A3699 /* normalizedByTests.swift */, 6613A9E81E841856004A3699 /* offsetByTests.swift */, 6613A9DC1E832913004A3699 /* rewriteTests.swift */, 6613A9E01E832B18004A3699 /* rewriteRangeTests.swift */, - 6613A9DE1E8329DF004A3699 /* rewriteTo.swift */, + 6613A9DE1E8329DF004A3699 /* rewriteToTests.swift */, 6613A9EC1E841C04004A3699 /* rubberBandedTests.swift */, 6613A9EA1E84188D004A3699 /* scaledByTests.swift */, 6695128E1E82ED0900D8868D /* slopTests.swift */, + 669512A01E830E1900D8868D /* startWithTests.swift */, 6613A9EE1E8420CE004A3699 /* thresholdTests.swift */, 6613A9F01E84214F004A3699 /* thresholdRangeTests.swift */, 6695129C1E830B6E00D8868D /* upperBoundTests.swift */, @@ -656,7 +656,7 @@ 6613A9F51E842FF5004A3699 /* yLockedToTests.swift in Sources */, 669512891E82E8CB00D8868D /* _rememberTests.swift in Sources */, 669512741E82E68900D8868D /* SubtractableTests.swift in Sources */, - 6613A9DF1E8329DF004A3699 /* rewriteTo.swift in Sources */, + 6613A9DF1E8329DF004A3699 /* rewriteToTests.swift in Sources */, 6613A9DD1E832913004A3699 /* rewriteTests.swift in Sources */, 6613A9DB1E832779004A3699 /* mergeTests.swift in Sources */, 6695129B1E830AE500D8868D /* lowerBoundTests.swift in Sources */, @@ -666,9 +666,9 @@ 6686F01C1E77293100F97CC4 /* MotionRuntimeTests.swift in Sources */, 669512931E8301D100D8868D /* dedupeTests.swift in Sources */, 6613A9F31E842F8B004A3699 /* xLockedToTests.swift in Sources */, - 6613A9E71E84170B004A3699 /* normalizeTests.swift in Sources */, + 6613A9E71E84170B004A3699 /* normalizedByTests.swift in Sources */, 6613A9EB1E84188D004A3699 /* scaledByTests.swift in Sources */, - 669512A11E830E1900D8868D /* initialValueTests.swift in Sources */, + 669512A11E830E1900D8868D /* startWithTests.swift in Sources */, 669512871E82E8CB00D8868D /* _mapTests.swift in Sources */, 6613A9EF1E8420CE004A3699 /* thresholdTests.swift in Sources */, 6695128B1E82E8CB00D8868D /* yTests.swift in Sources */, diff --git a/src/operators/normalized.swift b/src/operators/normalizedBy.swift similarity index 100% rename from src/operators/normalized.swift rename to src/operators/normalizedBy.swift diff --git a/src/operators/initialValue.swift b/src/operators/startWith.swift similarity index 60% rename from src/operators/initialValue.swift rename to src/operators/startWith.swift index 4b4ffd1..4dac810 100644 --- a/src/operators/initialValue.swift +++ b/src/operators/startWith.swift @@ -19,11 +19,19 @@ import Foundation extension MotionObservableConvertible { /** - Emits the provided value and then subscribes upstream and emits all subsequent values with no - modification. + Emits the provided value and then emits the values emitted by the upstream. - Helpful for priming a stream with an initial value. + The returned stream will cache the last value received and immediately emit it on subscription. + The returned stream is therefor guaranteed to always immediately emit a value upon subscription. */ + public func startWith(_ value: T) -> MotionObservable { + return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint, args: [value]))) { observer in + observer.next(value) + return self.asStream().subscribeAndForward(to: observer).unsubscribe + }._remember() + } + + @available(*, deprecated, message: "Use startWith() instead.") public func initialValue(_ value: T) -> MotionObservable { return MotionObservable(self.metadata.createChild(Metadata(#function, type: .constraint, args: [value]))) { observer in observer.next(value) diff --git a/tests/unit/operator/initialValueTests.swift b/tests/unit/operator/initialValueTests.swift deleted file mode 100644 index f255306..0000000 --- a/tests/unit/operator/initialValueTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -/* - Copyright 2017-present The Material Motion Authors. All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import XCTest -import MaterialMotion - -class initialValueTests: XCTestCase { - - func testInitialValueIsReceivedFirst() { - let property = createProperty() - - var values: [CGFloat] = [] - let subscription = property.initialValue(10).subscribeToValue { value in - values.append(value) - } - - property.value = -10 - - XCTAssertEqual(values, [10, 0, -10]) - - subscription.unsubscribe() - } -} diff --git a/tests/unit/operator/normalizeTests.swift b/tests/unit/operator/normalizedByTests.swift similarity index 100% rename from tests/unit/operator/normalizeTests.swift rename to tests/unit/operator/normalizedByTests.swift diff --git a/tests/unit/operator/rewriteTo.swift b/tests/unit/operator/rewriteToTests.swift similarity index 100% rename from tests/unit/operator/rewriteTo.swift rename to tests/unit/operator/rewriteToTests.swift diff --git a/tests/unit/operator/startWithTests.swift b/tests/unit/operator/startWithTests.swift new file mode 100644 index 0000000..7ce5a6e --- /dev/null +++ b/tests/unit/operator/startWithTests.swift @@ -0,0 +1,96 @@ +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import XCTest +import IndefiniteObservable +import MaterialMotion + +class startWithTests: XCTestCase { + + func testOverwrittenByReactivePropertyDefaultValue() { + let property = createProperty(withInitialValue: 0) + + var values: [CGFloat] = [] + let subscription = property.startWith(10).subscribeToValue { value in + values.append(value) + } + + property.value = -10 + + XCTAssertEqual(values, [0, -10]) + + subscription.unsubscribe() + } + + func testInitializedWithInitialValue() { + var valueObserver: MotionObserver? + let observable = MotionObservable { observer in + valueObserver = observer + return noopDisconnect + } + + var values: [CGFloat] = [] + let subscription = observable.startWith(10).subscribeToValue { value in + values.append(value) + } + + valueObserver?.next(50) + + XCTAssertEqual(values, [10, 50]) + + subscription.unsubscribe() + } + + func testAdditionalSubscriptionsReceiveLatestValue() { + var valueObserver: MotionObserver? + let observable = MotionObservable { observer in + valueObserver = observer + return noopDisconnect + } + + let stream = observable.startWith(10) + + let subscription = stream.subscribeToValue { value in } + + valueObserver?.next(50) + + var secondValues: [CGFloat] = [] + let secondSubscription = stream.subscribeToValue { value in + secondValues.append(value) + } + + XCTAssertEqual(secondValues, [50]) + + subscription.unsubscribe() + secondSubscription.unsubscribe() + } + + @available(*, deprecated) + func testDeprecatedInitialValueIsReceivedFirst() { + let property = createProperty() + + var values: [CGFloat] = [] + let subscription = property.initialValue(10).subscribeToValue { value in + values.append(value) + } + + property.value = -10 + + XCTAssertEqual(values, [10, 0, -10]) + + subscription.unsubscribe() + } +}