Skip to content

Commit

Permalink
Synchronous Feedback Loop (breaking change) (#51)
Browse files Browse the repository at this point in the history
* Synchronous Feedback Loop.

* Deprecate `Feedback.init(deriving:effects:)`.

* Add a text input example.

* Introduce a standalone `FeedbackLoop` type. Less state copes in the Property implementation.

* Restore docs for `Feedback.custom`.

* Rename file and address suggestions

* Make init public as all our convenance extensions calling it

* Add backwards compatibility mode via retaining sheduler

* Add backwards compatibility support via introducing FeedbackLoop.Feedback

* Fix Example app

* Restore old system tests

* Reducer to be `(inout State, Event) -> Void`

Co-authored-by: sergdort <[email protected]>
  • Loading branch information
andersio and sergdort authored Feb 26, 2020
1 parent 756e55f commit 893a98f
Show file tree
Hide file tree
Showing 10 changed files with 758 additions and 11 deletions.
33 changes: 26 additions & 7 deletions Example/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="zMS-h2-fCg">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="zMS-h2-fCg">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
Expand All @@ -21,6 +18,7 @@
</tabBar>
<connections>
<segue destination="BYZ-38-t0r" kind="relationship" relationship="viewControllers" id="zqG-cS-ENL"/>
<segue destination="knD-YS-64o" kind="relationship" relationship="viewControllers" id="pa0-al-SKR"/>
<segue destination="Zwd-ec-GM6" kind="relationship" relationship="viewControllers" id="Y8q-do-nau"/>
</connections>
</tabBarController>
Expand Down Expand Up @@ -76,6 +74,25 @@
</objects>
<point key="canvasLocation" x="-105" y="221"/>
</scene>
<!--Text-->
<scene sceneID="VWW-kx-XvC">
<objects>
<viewController id="knD-YS-64o" customClass="TextInputViewController" customModule="Example" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="ul4-g5-KYw"/>
<viewControllerLayoutGuide type="bottom" id="eRb-H5-rl8"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="u2P-e8-Jd3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
</view>
<tabBarItem key="tabBarItem" title="Text" image="pencil.circle" catalog="system" selectedImage="pencil.circle.fill" id="aMU-ht-hXF"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="1NA-Mx-QxK" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="265" y="894"/>
</scene>
<!--Movies-->
<scene sceneID="9oy-nA-Fdp">
<objects>
Expand All @@ -98,7 +115,7 @@
<rect key="frame" x="0.0" y="0.0" width="100" height="175"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="wCE-LK-fSd">
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="wCE-LK-fSd">
<rect key="frame" x="0.0" y="0.0" width="100" height="150"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="OdR-FR-C6U">
Expand Down Expand Up @@ -144,5 +161,7 @@
<resources>
<image name="counter" width="25" height="25"/>
<image name="movie" width="25" height="25"/>
<image name="pencil.circle" catalog="system" width="64" height="60"/>
<image name="pencil.circle.fill" catalog="system" width="64" height="60"/>
</resources>
</document>
83 changes: 83 additions & 0 deletions Example/TextInputViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import UIKit
import ReactiveSwift
import ReactiveCocoa
import ReactiveFeedback


class TextInputViewController: UIViewController {
let viewModel = TextInputViewModel()
let textView = UITextView()
let inputToolbar = UIToolbar(frame: UIScreen.main.bounds)
let characterCountLabel = UILabel()

override var inputAccessoryView: UIView? {
inputToolbar.frame.size = inputToolbar.sizeThatFits(UIScreen.main.bounds.size)
return inputToolbar
}

override func loadView() {
self.view = textView

textView.font = UIFont.preferredFont(forTextStyle: .title3)
textView.alwaysBounceVertical = true
textView.keyboardDismissMode = .interactive

if #available(iOS 11.0, *) {
textView.contentInsetAdjustmentBehavior = .always
} else {
self.automaticallyAdjustsScrollViewInsets = true
}
}

override func viewDidLoad() {
super.viewDidLoad()
viewModel.state.start()

textView.reactive.continuousTextValues
.take(duringLifetimeOf: self)
.observeValues { [viewModel] in viewModel.textDidChange($0) }

textView.reactive.text <~ viewModel.state.producer
characterCountLabel.reactive.text <~ viewModel.state.producer
.map { "\($0.count) characters" }
inputToolbar.setItems([UIBarButtonItem(customView: characterCountLabel)], animated: false)
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

textView.becomeFirstResponder()
}
}

final class TextInputViewModel {
let state: FeedbackLoop<String, Event>
private let (text, textObserver) = Signal<String, Never>.pipe()

init() {
self.state = FeedbackLoop<String, Event>(
initial: "Lorem ipsum ",
reduce: TextInputViewModel.reduce,
feedbacks: [.custom { [text] (state, consumer) in
text.producer.map(Event.update).enqueue(to: consumer).start()
}]
)
}

func textDidChange(_ text: String) {
textObserver.send(value: text)
}
}

extension TextInputViewModel {
static func reduce(state: inout String, event: Event) {
switch event {
case let .update(text):
state = text
}
}

enum Event {
case update(String)
}
}
44 changes: 44 additions & 0 deletions ReactiveFeedback.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,29 @@
objects = {

/* Begin PBXBuildFile section */
250B70DF23FC441300848429 /* FeedbackLoopSystemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 250B70DE23FC441300848429 /* FeedbackLoopSystemTests.swift */; };
250B70E023FC441300848429 /* FeedbackLoopSystemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 250B70DE23FC441300848429 /* FeedbackLoopSystemTests.swift */; };
250B70E123FC441300848429 /* FeedbackLoopSystemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 250B70DE23FC441300848429 /* FeedbackLoopSystemTests.swift */; };
25E1D2211F5493D000D90192 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25E1D2201F5493D000D90192 /* AppDelegate.swift */; };
25E1D2231F5493D000D90192 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25E1D2221F5493D000D90192 /* ViewController.swift */; };
25E1D2261F5493D000D90192 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 25E1D2241F5493D000D90192 /* Main.storyboard */; };
25E1D2281F5493D000D90192 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 25E1D2271F5493D000D90192 /* Assets.xcassets */; };
25E1D22B1F5493D000D90192 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 25E1D2291F5493D000D90192 /* LaunchScreen.storyboard */; };
25E1D2381F56091A00D90192 /* PaginationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25E1D2371F56091A00D90192 /* PaginationViewController.swift */; };
5898B6D11F97ADDD005EEAEC /* SystemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898B6D01F97ADDD005EEAEC /* SystemTests.swift */; };
656A9C9323D0813500EFB2F8 /* FeedbackLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656A9C9223D0813500EFB2F8 /* FeedbackLoop.swift */; };
656A9C9423D0813500EFB2F8 /* FeedbackLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656A9C9223D0813500EFB2F8 /* FeedbackLoop.swift */; };
656A9C9523D0813500EFB2F8 /* FeedbackLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656A9C9223D0813500EFB2F8 /* FeedbackLoop.swift */; };
656A9C9723D0826100EFB2F8 /* FeedbackEventConsumer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656A9C9623D0826100EFB2F8 /* FeedbackEventConsumer.swift */; };
656A9C9823D0826100EFB2F8 /* FeedbackEventConsumer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656A9C9623D0826100EFB2F8 /* FeedbackEventConsumer.swift */; };
656A9C9923D0826100EFB2F8 /* FeedbackEventConsumer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656A9C9623D0826100EFB2F8 /* FeedbackEventConsumer.swift */; };
65761B2623CF20EF004D5506 /* Floodgate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65761B2523CF20EF004D5506 /* Floodgate.swift */; };
65761B2723CF20EF004D5506 /* Floodgate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65761B2523CF20EF004D5506 /* Floodgate.swift */; };
65761B2823CF20EF004D5506 /* Floodgate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65761B2523CF20EF004D5506 /* Floodgate.swift */; };
65761B2E23CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65761B2D23CF4CA1004D5506 /* NSLock+Extensions.swift */; };
65761B2F23CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65761B2D23CF4CA1004D5506 /* NSLock+Extensions.swift */; };
65761B3023CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65761B2D23CF4CA1004D5506 /* NSLock+Extensions.swift */; };
65761B3223CF677F004D5506 /* TextInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65761B3123CF677F004D5506 /* TextInputViewController.swift */; };
65F8C260218371A800924657 /* Feedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95097E70D3CBFF05FA7B8CC /* Feedback.swift */; };
65F8C261218371A800924657 /* SignalProducer+System.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9509880213192F0D80EC2B3 /* SignalProducer+System.swift */; };
65F8C262218371A800924657 /* Property+System.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD5D42C1F97375E00E6AE5A /* Property+System.swift */; };
Expand Down Expand Up @@ -122,6 +138,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
250B70DE23FC441300848429 /* FeedbackLoopSystemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackLoopSystemTests.swift; sourceTree = "<group>"; };
25CC87AE1F92855300A6EBFC /* ReactiveFeedback.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReactiveFeedback.framework; sourceTree = BUILT_PRODUCTS_DIR; };
25CC87B11F92855300A6EBFC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
25E1D21D1F5493D000D90192 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand All @@ -134,6 +151,11 @@
25E1D2371F56091A00D90192 /* PaginationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginationViewController.swift; sourceTree = "<group>"; };
587F0720201F647400ACD219 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
5898B6D01F97ADDD005EEAEC /* SystemTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemTests.swift; sourceTree = "<group>"; };
656A9C9223D0813500EFB2F8 /* FeedbackLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackLoop.swift; sourceTree = "<group>"; };
656A9C9623D0826100EFB2F8 /* FeedbackEventConsumer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackEventConsumer.swift; sourceTree = "<group>"; };
65761B2523CF20EF004D5506 /* Floodgate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Floodgate.swift; sourceTree = "<group>"; };
65761B2D23CF4CA1004D5506 /* NSLock+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLock+Extensions.swift"; sourceTree = "<group>"; };
65761B3123CF677F004D5506 /* TextInputViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputViewController.swift; sourceTree = "<group>"; };
65F8C26B218371A800924657 /* ReactiveFeedback.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReactiveFeedback.framework; sourceTree = BUILT_PRODUCTS_DIR; };
65F8C27A218371AC00924657 /* ReactiveFeedback.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReactiveFeedback.framework; sourceTree = BUILT_PRODUCTS_DIR; };
65F8C28E2183723F00924657 /* ReactiveFeedbackTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactiveFeedbackTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -223,9 +245,13 @@
isa = PBXGroup;
children = (
A95097E70D3CBFF05FA7B8CC /* Feedback.swift */,
656A9C9223D0813500EFB2F8 /* FeedbackLoop.swift */,
656A9C9623D0826100EFB2F8 /* FeedbackEventConsumer.swift */,
A9509880213192F0D80EC2B3 /* SignalProducer+System.swift */,
9AD5D42C1F97375E00E6AE5A /* Property+System.swift */,
25CC87B11F92855300A6EBFC /* Info.plist */,
65761B2523CF20EF004D5506 /* Floodgate.swift */,
65761B2D23CF4CA1004D5506 /* NSLock+Extensions.swift */,
);
path = ReactiveFeedback;
sourceTree = "<group>";
Expand Down Expand Up @@ -263,6 +289,7 @@
25E1D2201F5493D000D90192 /* AppDelegate.swift */,
25E1D2221F5493D000D90192 /* ViewController.swift */,
25E1D2371F56091A00D90192 /* PaginationViewController.swift */,
65761B3123CF677F004D5506 /* TextInputViewController.swift */,
25E1D2241F5493D000D90192 /* Main.storyboard */,
25E1D2271F5493D000D90192 /* Assets.xcassets */,
25E1D2291F5493D000D90192 /* LaunchScreen.storyboard */,
Expand All @@ -276,6 +303,7 @@
children = (
5898B6D01F97ADDD005EEAEC /* SystemTests.swift */,
9AE181BA1F95A71B00A07551 /* Info.plist */,
250B70DE23FC441300848429 /* FeedbackLoopSystemTests.swift */,
);
path = ReactiveFeedbackTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -568,14 +596,19 @@
files = (
A9509BE4551098F4A5503820 /* Feedback.swift in Sources */,
A950943401765BB90FA846B2 /* SignalProducer+System.swift in Sources */,
65761B2E23CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */,
9AD5D42D1F97375E00E6AE5A /* Property+System.swift in Sources */,
656A9C9323D0813500EFB2F8 /* FeedbackLoop.swift in Sources */,
656A9C9723D0826100EFB2F8 /* FeedbackEventConsumer.swift in Sources */,
65761B2623CF20EF004D5506 /* Floodgate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
25E1D2191F5493D000D90192 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
65761B3223CF677F004D5506 /* TextInputViewController.swift in Sources */,
25E1D2231F5493D000D90192 /* ViewController.swift in Sources */,
25E1D2381F56091A00D90192 /* PaginationViewController.swift in Sources */,
25E1D2211F5493D000D90192 /* AppDelegate.swift in Sources */,
Expand All @@ -588,7 +621,11 @@
files = (
65F8C260218371A800924657 /* Feedback.swift in Sources */,
65F8C261218371A800924657 /* SignalProducer+System.swift in Sources */,
65761B2F23CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */,
65F8C262218371A800924657 /* Property+System.swift in Sources */,
656A9C9423D0813500EFB2F8 /* FeedbackLoop.swift in Sources */,
656A9C9823D0826100EFB2F8 /* FeedbackEventConsumer.swift in Sources */,
65761B2723CF20EF004D5506 /* Floodgate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -598,7 +635,11 @@
files = (
65F8C26F218371AC00924657 /* Feedback.swift in Sources */,
65F8C270218371AC00924657 /* SignalProducer+System.swift in Sources */,
65761B3023CF4CA2004D5506 /* NSLock+Extensions.swift in Sources */,
65F8C271218371AC00924657 /* Property+System.swift in Sources */,
656A9C9523D0813500EFB2F8 /* FeedbackLoop.swift in Sources */,
656A9C9923D0826100EFB2F8 /* FeedbackEventConsumer.swift in Sources */,
65761B2823CF20EF004D5506 /* Floodgate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -607,6 +648,7 @@
buildActionMask = 2147483647;
files = (
65F8C2802183723F00924657 /* SystemTests.swift in Sources */,
250B70E023FC441300848429 /* FeedbackLoopSystemTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -615,6 +657,7 @@
buildActionMask = 2147483647;
files = (
65F8C2942183725900924657 /* SystemTests.swift in Sources */,
250B70E123FC441300848429 /* FeedbackLoopSystemTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -623,6 +666,7 @@
buildActionMask = 2147483647;
files = (
5898B6D11F97ADDD005EEAEC /* SystemTests.swift in Sources */,
250B70DF23FC441300848429 /* FeedbackLoopSystemTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
19 changes: 19 additions & 0 deletions ReactiveFeedback/FeedbackEventConsumer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

public class FeedbackEventConsumer<Event> {
struct Token: Equatable {
let value: UUID

init() {
value = UUID()
}
}

func process(_ event: Event, for token: Token) {
fatalError("This is an abstract class. You must subclass this and provide your own implementation")
}

func dequeueAllEvents(for token: Token) {
fatalError("This is an abstract class. You must subclass this and provide your own implementation")
}
}
Loading

0 comments on commit 893a98f

Please sign in to comment.