├── .bundle └── config ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build-test.yml ├── .gitignore ├── .swiftlint.yml ├── Example ├── Example_Tests │ ├── CounterStoreTests.swift │ ├── FoodOnboardingTests.swift │ ├── Info.plist │ ├── LoginTests.swift │ ├── MyDayFlowTests.swift │ ├── MyDayTests.swift │ ├── OnboardingTests.swift │ ├── SearchAndTrackFlowTests.swift │ ├── SearchStoreTests_FunctionalStyle1.swift │ ├── SearchStoreTests_FunctionalStyle2.swift │ ├── SearchStoreTests_LassoStoreTestCase.swift │ ├── SearchStoreTests_XCTestCase.swift │ ├── Signup │ │ ├── SignupFormStoreTests.swift │ │ └── SignupIntroStoreTests.swift │ └── SurveyFlowTests.swift ├── Lasso-SwiftUI │ ├── Lasso-SwiftUI.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ ├── IDETemplateMacros.plist │ │ │ └── xcschemes │ │ │ └── Lasso-SwiftUI.xcscheme │ ├── Lasso-SwiftUI │ │ ├── AppCatalog │ │ │ ├── AppCatalogFlow.swift │ │ │ └── AppCatalogScreen │ │ │ │ ├── AppCatalogScreen.swift │ │ │ │ └── AppCatalogView.swift │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── buttonBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── buttonDisabledBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── buttonForeground.colorset │ │ │ │ └── Contents.json │ │ │ └── buttonPressedBackground.colorset │ │ │ │ └── Contents.json │ │ ├── Components │ │ │ ├── ActivityIndicator.swift │ │ │ └── NavigationRow.swift │ │ ├── Info.plist │ │ ├── LaunchScreen.storyboard │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── Samples │ │ │ ├── Flows │ │ │ │ └── WelcomeOnboarding │ │ │ │ │ ├── WelcomeMessage.swift │ │ │ │ │ ├── WelcomeMessageView.swift │ │ │ │ │ └── WelcomeOnboardingFlow.swift │ │ │ └── Screens │ │ │ │ ├── Login │ │ │ │ ├── Login.swift │ │ │ │ ├── LoginService.swift │ │ │ │ ├── LoginStore.swift │ │ │ │ └── LoginView.swift │ │ │ │ └── SimpleCounter │ │ │ │ ├── SimpleCounter.swift │ │ │ │ ├── SimpleCounterStore.swift │ │ │ │ └── SimpleCounterView.swift │ │ ├── Style │ │ │ ├── ButtonStyle+App.swift │ │ │ ├── Color+Style.swift │ │ │ └── TextFieldStyle+App.swift │ │ └── Utilities │ │ │ ├── EdgeInsets+Helpers.swift │ │ │ ├── SelfIdentifiable.swift │ │ │ └── String+CamelCase.swift │ └── Lasso-SwiftUITests │ │ ├── Info.plist │ │ └── Lasso_SwiftUITests.swift ├── Lasso.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDETemplateMacros.plist │ │ └── xcschemes │ │ └── Lasso-Example.xcscheme ├── Lasso.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDETemplateMacros.plist │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Lasso │ ├── AppDelegate.swift │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── circle.imageset │ │ │ ├── Contents.json │ │ │ └── circle.pdf │ │ └── square.imageset │ │ │ ├── Contents.json │ │ │ └── square.pdf │ ├── Info.plist │ ├── Presentation │ │ ├── SimpleCounter.swift │ │ └── Welcome.swift │ ├── Sample Catalog │ │ ├── SampleCatalog.swift │ │ ├── SampleCatalogFlow.swift │ │ └── SampleCatalogViewController.swift │ ├── Samples │ │ ├── ChooseWindowTransition.swift │ │ ├── Counter │ │ │ ├── CounterScreen.swift │ │ │ └── CounterViewController.swift │ │ ├── Login │ │ │ ├── Login.swift │ │ │ ├── LoginService.swift │ │ │ └── LoginViewController.swift │ │ ├── MyDay │ │ │ ├── CalendarScreen.swift │ │ │ ├── DailyLogScreen.swift │ │ │ ├── MyDayCardsScreen.swift │ │ │ ├── MyDayController.swift │ │ │ └── MyDayFlow.swift │ │ ├── Onboarding │ │ │ ├── EnterNameScreen.swift │ │ │ ├── FoodOnboarding.swift │ │ │ └── Onboarding.swift │ │ ├── PageControllerFlow.swift │ │ ├── SearchAndTrackFlow.swift │ │ ├── SearchModule.swift │ │ ├── Signup │ │ │ ├── Signup.swift │ │ │ ├── SignupForm.swift │ │ │ └── SignupIntro.swift │ │ ├── SplitView │ │ │ ├── RandomItemFlow.swift │ │ │ └── SplitView.swift │ │ ├── StrangeFlow.swift │ │ ├── Survey │ │ │ ├── MakeListModule.swift │ │ │ ├── SurveyFlow.swift │ │ │ └── SurveyModule.swift │ │ ├── Tabs │ │ │ ├── RandomItems.swift │ │ │ └── TabsFlow.swift │ │ ├── TextScreen.swift │ │ ├── UIKitBindings.swift │ │ └── ViewModules │ │ │ ├── SubViewsShowcase.swift │ │ │ ├── SubViewsShowcaseViewController.swift │ │ │ └── ViewModules.swift │ └── Utilities │ │ ├── Compatibility.swift │ │ ├── RandomStrings.swift │ │ ├── Result.swift │ │ └── ViewHelpers │ │ ├── UIActivityIndicatorView.swift │ │ ├── UIButton.swift │ │ ├── UIColor.swift │ │ ├── UIImage.swift │ │ ├── UILabel.swift │ │ ├── UITableView.swift │ │ ├── UITextField.swift │ │ └── UIView.swift ├── LassoTestUtilities_Tests │ ├── AssertionTimeoutTests.swift │ ├── FowTestCaseSetupTests.swift │ ├── Info.plist │ ├── LifeCycleController.swift │ ├── MockableExtensionTests.swift │ ├── ModalTestingTests+AnyModalPresentationStyle.swift │ ├── ModalTestingTests+FullScreen.swift │ ├── ModalTestingTests+SystemBehavior.swift │ ├── NavigationTestingTests.swift │ ├── ValueDiffingTests.swift │ └── VerboseLoggingTests.swift ├── Lasso_Tests │ ├── ActionDispatchableBindingTests.swift │ ├── Info.plist │ ├── MockableTests.swift │ ├── ObjectBindingTests.swift │ ├── ScreenCaptureStoreTests.swift │ ├── StateObservationTests.swift │ ├── UIGestureRecognizer+SendActions.swift │ └── ValueBinderTests.swift ├── Podfile ├── Podfile.lock └── SwiftPM │ ├── Images │ ├── app.gif │ └── dependencies.png │ ├── Lasso-SwiftPM │ ├── Lasso-SwiftPM.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ ├── IDETemplateMacros.plist │ │ │ └── xcschemes │ │ │ └── Lasso-SwiftPM.xcscheme │ ├── Lasso-SwiftPM │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── Info.plist │ │ ├── RestScreen.swift │ │ ├── WorkFlow.swift │ │ └── WorkScreen.swift │ └── Lasso-SwiftPMTests │ │ ├── Info.plist │ │ ├── RestScreenTests.swift │ │ ├── WorkFlowTests.swift │ │ └── WorkScreenTests.swift │ └── ReadMe.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Lasso.podspec ├── LassoTestUtilities.podspec ├── Package.swift ├── README.md ├── Sources ├── Lasso │ ├── Flow │ │ ├── Flow+UINavigationController.swift │ │ ├── Flow+UIPageViewController.swift │ │ ├── Flow+UIViewController.swift │ │ └── Flow.swift │ ├── Module.swift │ ├── Screen │ │ ├── Screen.swift │ │ └── ScreenFactory.swift │ ├── ScreenPlacer │ │ ├── ScreenPlacer+Abstract.swift │ │ ├── ScreenPlacer+UINavigationController.swift │ │ ├── ScreenPlacer+UIPageViewController.swift │ │ ├── ScreenPlacer+UITabBarController.swift │ │ ├── ScreenPlacer+UIViewController.swift │ │ ├── ScreenPlacer+UIWindow.swift │ │ ├── ScreenPlacer.swift │ │ └── ScreenPlacerEmbedding.swift │ ├── Store │ │ ├── MockStore.swift │ │ ├── PassthroughStore.swift │ │ ├── Store.swift │ │ └── ViewStore.swift │ ├── SwiftUI+Lasso │ │ ├── AlertButton+Lasso.swift │ │ ├── DynamicViewContent+Lasso.swift │ │ ├── SwiftUI+Bindings.swift │ │ └── SwiftUI+Lasso.swift │ ├── Types │ │ ├── ActionDispatchable+Bindings.swift │ │ ├── Mockable.swift │ │ ├── ObjectBinding.swift │ │ ├── OutputBridge.swift │ │ ├── Types.swift │ │ └── ValueBinder.swift │ └── UIKit+Lasso │ │ └── UIWindow+Transition.swift └── LassoTestUtilities │ ├── ControllerLifecycle.swift │ ├── Fail.swift │ ├── FlowAsserting.swift │ ├── FlowTestCase.swift │ ├── LassoStoreTestCase.swift │ ├── ModalTesting.swift │ ├── NavigationTesting.swift │ ├── ScreenModule+Testing.swift │ ├── StoreTesting+ThenAssertion.swift │ ├── StoreTesting+WhenStatement.swift │ ├── StoreTesting.swift │ ├── TypeCast.swift │ ├── ValueDiffing.swift │ ├── VerboseLogging.swift │ └── XCTestCase+Mockable.swift └── docs ├── Lasso-FlowsIntro.md ├── Lasso-Introduction-part1.md ├── declaring-types-notes.md ├── images ├── Lasso_Logo.pdf ├── Lasso_Logo.png ├── Lasso_Logo.svg ├── TutorialFlowDiagram.svg ├── flow-flow-screen.png ├── flow-flow-screen.svg ├── flow-flow.svg ├── flow.gif ├── flow.png ├── flow.svg ├── flow1.svg ├── login.png ├── screen-no output.svg ├── screen.png ├── screen.svg ├── trevor-screens.png ├── view-store.png └── view-store.svg ├── memory-management.md └── style-guide.md /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_FORCE_RUBY_PLATFORM: "true" 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | 3 | # These owners will be the default owners for everything in 4 | # the repo. Unless a later match takes precedence, 5 | # the following people will be requested for review 6 | # when someone opens a pull request. 7 | * @g-mark @trevor-beasty 8 | 9 | # Order is important; the last matching pattern takes the most 10 | # precedence. When someone opens a pull request that only 11 | # modifies JS files, only @js-owner and not the global 12 | # owner(s) will be requested for a review. 13 | #*.js @js-owner 14 | 15 | # You can also use email addresses if you prefer. They'll be 16 | # used to look up users just like we do for commit author 17 | # emails. 18 | #*.go docs@example.com 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Issue # 2 | 3 | ## Describe your changes 4 | 5 | *Clearly and concisely describe what's in this pull request. Include screenshots, if necessary.* 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xccheckout 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | Packages/ 41 | Package.pins 42 | Package.resolved 43 | .build/ 44 | .swiftpm/ 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | Example/Pods/ 53 | 54 | # Miscellaneous 55 | .idea 56 | .env 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | Package.resolved 70 | 71 | ### Tuist derived files ### 72 | **/Derived/ 73 | **/dump.* 74 | **/graph.json 75 | **/graph.dot 76 | **/LassoTuist.* 77 | **/LassoTuistExample.* 78 | 79 | ### Tuist managed dependencies ### 80 | **/Tuist/Dependencies -------------------------------------------------------------------------------- /Example/Example_Tests/CounterStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ==----------------------------------------------------------------------== // 3 | // 4 | // CounterStoreTests.swift 5 | // 6 | // Created by Steven Grosmark on 10/5/19. 7 | // 8 | // 9 | // This source file is part of the Lasso open source project 10 | // 11 | // https://github.com/ww-tech/lasso 12 | // 13 | // Copyright © 2019-2020 WW International, Inc. 14 | // 15 | // ==----------------------------------------------------------------------== // 16 | // 17 | 18 | import XCTest 19 | import LassoTestUtilities 20 | @testable import Lasso_Example 21 | 22 | class CounterStoreTests: XCTestCase, LassoStoreTestCase { 23 | 24 | let testableStore = TestableStore() 25 | 26 | override func setUp() { 27 | super.setUp() 28 | store = CounterStore() 29 | XCTAssertEqual(store.state.counter, 0) 30 | } 31 | 32 | func test_IncrementDecrement() { 33 | // when - tap + 3x 34 | for _ in 0..<3 { 35 | store.dispatchAction(.didTapIncrement) 36 | } 37 | 38 | // then - counter should be 3 39 | XCTAssertStateEquals(updatedMarker { state in 40 | state.counter = 3 41 | }) 42 | 43 | // when - tap decrement 44 | store.dispatchAction(.didTapDecrement) 45 | 46 | // then - counter should go down 47 | XCTAssertStateEquals(updatedMarker { state in 48 | state.counter = 2 49 | }) 50 | 51 | // sanity check no outputs generated 52 | XCTAssertOutputs([]) 53 | } 54 | 55 | func test_Decrement_Clamping() { 56 | // when 57 | store.dispatchAction(.didTapDecrement) 58 | 59 | // then - don't go negative 60 | XCTAssertEqual(store.state.counter, 0) 61 | } 62 | 63 | func test_Next() { 64 | store.dispatchAction(.didTapNext) 65 | XCTAssertOutputs([.didTapNext]) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Example/Example_Tests/FoodOnboardingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ==----------------------------------------------------------------------== // 3 | // 4 | // FoodOnboardingTests.swift 5 | // 6 | // Created by Trevor Beasty on 7/16/19. 7 | // 8 | // 9 | // This source file is part of the Lasso open source project 10 | // 11 | // https://github.com/ww-tech/lasso 12 | // 13 | // Copyright © 2019-2020 WW International, Inc. 14 | // 15 | // ==----------------------------------------------------------------------== // 16 | // 17 | 18 | import XCTest 19 | import Lasso 20 | import LassoTestUtilities 21 | @testable import Lasso_Example 22 | 23 | class FoodOnboardingTests: FlowTestCase { 24 | 25 | var flow: FoodOnboardingFlow! 26 | 27 | override func setUp() { 28 | super.setUp() 29 | flow = FoodOnboardingFlow() 30 | } 31 | 32 | override func tearDown() { 33 | flow = nil 34 | super.tearDown() 35 | } 36 | 37 | func test() throws { 38 | typealias State = TextScreenModule.State 39 | 40 | // flow start 41 | let welcomeController: TextViewController = try assertRoot( 42 | of: navigationController, 43 | when: { flow.start(with: root(of: navigationController)) } 44 | ) 45 | 46 | XCTAssertEqual(welcomeController.store.state, State(title: "Welcome", 47 | description: "We just need a little info to get you going...", 48 | buttons: ["Next"])) 49 | 50 | // press next on welcome screen 51 | let notificationsController: TextViewController = try assertPushed( 52 | after: welcomeController, 53 | when: { welcomeController.store.dispatchAction(.didTapButton(0)) } 54 | ) 55 | 56 | XCTAssertEqual(notificationsController.store.state, State(title: "Notifications", 57 | description: "Enable Notifications for the smoothest experience...", 58 | buttons: ["Next"])) 59 | 60 | // press next on notifications screen 61 | let finishController: TextViewController = try assertPushed( 62 | after: notificationsController, 63 | when: { notificationsController.store.dispatchAction(.didTapButton(0)) } 64 | ) 65 | 66 | XCTAssertEqual(finishController.store.state, State(title: "All set", 67 | description: "Ok, let's get started!", 68 | buttons: ["Finish"])) 69 | 70 | // output 71 | flow.assert(when: { finishController.store.dispatchAction(.didTapButton(0)) }, 72 | outputs: .didFinish) 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /Example/Example_Tests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example/Example_Tests/SearchStoreTests_XCTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ==----------------------------------------------------------------------== // 3 | // 4 | // SearchStoreTests_XCTestCase.swift 5 | // 6 | // Created by Steven Grosmark on 9/19/19. 7 | // 8 | // 9 | // This source file is part of the Lasso open source project 10 | // 11 | // https://github.com/ww-tech/lasso 12 | // 13 | // Copyright © 2019-2020 WW International, Inc. 14 | // 15 | // ==----------------------------------------------------------------------== // 16 | // 17 | 18 | import XCTest 19 | @testable import Lasso_Example 20 | 21 | /// 22 | /// Example of Store unit testing w/no support utilities. 23 | /// 24 | 25 | class SearchStoreAltTests: XCTestCase { 26 | 27 | struct Item: SearchListRepresentable, Equatable { 28 | let searchListTitle: String 29 | } 30 | 31 | typealias Module = SearchScreenModule 32 | typealias State = Module.State 33 | 34 | var searchRequests = [(query: String?, completion: (Result<[Item], Error>) -> Void)]() 35 | 36 | var store: SearchStore! 37 | 38 | enum MockError: Error { 39 | case failed 40 | } 41 | 42 | override func setUp() { 43 | super.setUp() 44 | 45 | store = SearchStore(with: State(searchText: nil, items: [], phase: .idle, viewDidAppear: false)) 46 | store.getSearchResults = { self.searchRequests.append(($0, $1)) } 47 | searchRequests = [] 48 | store.dispatchAction(.viewWillAppear) 49 | } 50 | 51 | func test_InitialFetchOnViewWillAppear() { 52 | // TEST: loading state with pending request 53 | XCTAssertEqual(store.state, State(searchText: nil, items: [], phase: .searching, viewDidAppear: true)) 54 | XCTAssertEqual(self.searchRequests.count, 1) 55 | XCTAssertEqual(self.searchRequests[0].query, nil) 56 | } 57 | 58 | func test_RequestSuccess() { 59 | 60 | // when 61 | let items = [Item(searchListTitle: "a")] 62 | searchRequests[0].completion(.success(items)) 63 | 64 | // then 65 | XCTAssertEqual(store.state, State(searchText: nil, items: items, phase: .idle, viewDidAppear: true)) 66 | } 67 | 68 | func test_RequestFailure() { 69 | 70 | // when 71 | searchRequests[0].completion(.failure(MockError.failed)) 72 | 73 | // then 74 | XCTAssertEqual(store.state, State(searchText: nil, items: [], phase: .error(message: "Something went wrong"), viewDidAppear: true)) 75 | 76 | // when 77 | store.dispatchAction(.didAcknowledgeError) 78 | 79 | // then 80 | XCTAssertEqual(store.state, State(searchText: nil, items: [], phase: .idle, viewDidAppear: true)) 81 | } 82 | 83 | func test_Selection() { 84 | 85 | // given 86 | let items = [Item(searchListTitle: "a")] 87 | searchRequests[0].completion(.success(items)) 88 | var outputs = [Module.Output]() 89 | store.observeOutput { outputs.append($0) } 90 | 91 | // when 92 | store.dispatchAction(.didSelectItem(idx: 0)) 93 | 94 | // then 95 | XCTAssertEqual(outputs, [.didSelectItem(items[0])]) 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /Example/Example_Tests/Signup/SignupIntroStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ==----------------------------------------------------------------------== // 3 | // 4 | // SignupIntroStoreTests.swift 5 | // 6 | // Created by Steven Grosmark on 10/5/19. 7 | // 8 | // 9 | // This source file is part of the Lasso open source project 10 | // 11 | // https://github.com/ww-tech/lasso 12 | // 13 | // Copyright © 2019-2020 WW International, Inc. 14 | // 15 | // ==----------------------------------------------------------------------== // 16 | // 17 | 18 | import XCTest 19 | import Lasso 20 | import LassoTestUtilities 21 | @testable import Lasso_Example 22 | 23 | class SignupIntroStoreTests: XCTestCase, LassoStoreTestCase { 24 | 25 | let testableStore = TestableStore() 26 | 27 | override func setUp() { 28 | super.setUp() 29 | store = Store(with: EmptyState()) 30 | } 31 | 32 | func test_TheOneAndOnlyButton() { 33 | store.dispatchAction(.didTapNext) 34 | XCTAssertOutputs([.didTapNext]) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // ==----------------------------------------------------------------------== // 8 | // 9 | // ___FILENAME___ 10 | // 11 | // Created by ___FULLUSERNAME___ on ___DATE___ 12 | // 13 | // 14 | // This source file is part of the Lasso open source project 15 | // 16 | // https://github.com/ww-tech/lasso 17 | // 18 | // Copyright © 2019-___YEAR___ WW International, Inc. 19 | // 20 | // ==----------------------------------------------------------------------== // 21 | // 22 | 23 | 24 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/AppCatalog/AppCatalogFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ==----------------------------------------------------------------------== // 3 | // 4 | // AppCatalogFlow.swift 5 | // 6 | // Created by Steven Grosmark on 03/23/2021. 7 | // 8 | // 9 | // This source file is part of the Lasso open source project 10 | // 11 | // https://github.com/ww-tech/lasso 12 | // 13 | // Copyright © 2019-2021 WW International, Inc. 14 | // 15 | // ==----------------------------------------------------------------------== // 16 | // 17 | 18 | import UIKit 19 | import Lasso 20 | 21 | final class AppCatalogFlow: Flow { 22 | 23 | override func createInitialController() -> UIViewController { 24 | AppCatalogScreen 25 | .createScreen() 26 | .observeOutput { [weak self] in self?.handleOutput($0) } 27 | .controller 28 | } 29 | 30 | private func handleOutput(_ output: AppCatalogScreen.Output) { 31 | switch output { 32 | 33 | case .simpleCounter: 34 | SimpleCounter 35 | .createScreen() 36 | .place(with: nextPushedInFlow) 37 | 38 | case .login: 39 | Login 40 | .createScreen() 41 | .observeOutput { [weak self] _ in self?.unwind() } 42 | .place(with: nextPushedInFlow) 43 | 44 | case .welcome: 45 | WelcomeOnboardingFlow() 46 | .observeOutput { [weak self] _ in self?.unwind() } 47 | .start(with: nextPresentedInFlow?.withDismissibleNavigationEmbedding()) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/AppCatalog/AppCatalogScreen/AppCatalogScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ==----------------------------------------------------------------------== // 3 | // 4 | // AppCatalogScreen.swift 5 | // 6 | // Created by Steven Grosmark on 03/23/2021. 7 | // 8 | // 9 | // This source file is part of the Lasso open source project 10 | // 11 | // https://github.com/ww-tech/lasso 12 | // 13 | // Copyright © 2019-2021 WW International, Inc. 14 | // 15 | // ==----------------------------------------------------------------------== // 16 | // 17 | 18 | import UIKit 19 | import SwiftUI 20 | import Lasso 21 | 22 | enum AppCatalogScreen: ScreenModule { 23 | 24 | struct State: Equatable { 25 | let title: String 26 | let sections: [Section] 27 | } 28 | 29 | enum Section: SelfIdentifiable, TitleConvertible { 30 | 31 | case screens 32 | case flows 33 | 34 | var items: [Item] { 35 | switch self { 36 | case .screens: return [.simpleCounter, .login] 37 | case .flows: return [.welcome] 38 | } 39 | } 40 | 41 | enum Item: SelfIdentifiable, TitleConvertible { 42 | case simpleCounter, login 43 | case welcome 44 | } 45 | } 46 | 47 | typealias Action = Section.Item 48 | typealias Output = Action 49 | 50 | static func createScreen(with store: AppCatalogStore) -> Screen { 51 | Screen(store, AppCatalogView(store: store.asViewStore())) 52 | } 53 | 54 | static var defaultInitialState: State { State(title: "Lasso ❤️ SwiftUI", sections: [.screens, .flows]) } 55 | } 56 | 57 | final class AppCatalogStore: LassoStore { 58 | 59 | override func handleAction(_ action: Action) { 60 | dispatchOutput(action) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/AppCatalog/AppCatalogScreen/AppCatalogView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ==----------------------------------------------------------------------== // 3 | // 4 | // AppCatalogView.swift 5 | // 6 | // Created by Steven Grosmark on 03/23/2021. 7 | // 8 | // 9 | // This source file is part of the Lasso open source project 10 | // 11 | // https://github.com/ww-tech/lasso 12 | // 13 | // Copyright © 2019-2021 WW International, Inc. 14 | // 15 | // ==----------------------------------------------------------------------== // 16 | // 17 | 18 | import SwiftUI 19 | import Lasso 20 | 21 | struct AppCatalogView: View { 22 | 23 | @ObservedObject private(set) var store: AppCatalogScreen.ViewStore 24 | 25 | var body: some View { 26 | VStack { 27 | Text(store.state.title).padding() 28 | List { 29 | ForEach(store.state.sections) { section in 30 | Section(header: CatalogSectionHeader(section: section)) { 31 | ForEach(section.items) { item in 32 | NavigationRow(item.title, target: store, action: item) 33 | } 34 | } 35 | } 36 | }.listStyle(GroupedListStyle()) 37 | } 38 | .navigationBarTitle("Samples") 39 | } 40 | } 41 | 42 | private struct CatalogSectionHeader: View { 43 | let section: AppCatalogScreen.Section 44 | var body: some View { 45 | HStack { 46 | Image(systemName: section.systemImage) 47 | Text(section.title) 48 | } 49 | } 50 | } 51 | 52 | extension AppCatalogScreen.Section { 53 | 54 | fileprivate var systemImage: String { 55 | switch self { 56 | case .screens: return "doc" 57 | case .flows: return "doc.on.doc" 58 | } 59 | } 60 | } 61 | 62 | struct CatalogView_Previews: PreviewProvider { 63 | static var previews: some View { 64 | let store = AppCatalogStore(with: AppCatalogScreen.defaultInitialState) 65 | AppCatalogView(store: store.asViewStore()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ==----------------------------------------------------------------------== // 3 | // 4 | // AppDelegate.swift 5 | // 6 | // Created by Steven Grosmark on 03/23/2021. 7 | // 8 | // 9 | // This source file is part of the Lasso open source project 10 | // 11 | // https://github.com/ww-tech/lasso 12 | // 13 | // Copyright © 2019-2021 WW International, Inc. 14 | // 15 | // ==----------------------------------------------------------------------== // 16 | // 17 | 18 | import UIKit 19 | import Lasso 20 | 21 | @UIApplicationMain 22 | class AppDelegate: UIResponder, UIApplicationDelegate { 23 | 24 | var window: UIWindow? 25 | 26 | func application(_ application: UIApplication, 27 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 28 | 29 | let window = UIWindow(frame: UIScreen.main.bounds) 30 | self.window = window 31 | window.backgroundColor = .white 32 | 33 | AppCatalogFlow().start(with: root(of: window).withNavigationEmbedding()) 34 | 35 | window.makeKeyAndVisible() 36 | 37 | return true 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/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 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/Assets.xcassets/buttonBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.969", 9 | "green" : "0.503", 10 | "red" : "0.584" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.969", 27 | "green" : "0.503", 28 | "red" : "0.584" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/Assets.xcassets/buttonDisabledBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.572" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | }, 13 | { 14 | "appearances" : [ 15 | { 16 | "appearance" : "luminosity", 17 | "value" : "dark" 18 | } 19 | ], 20 | "color" : { 21 | "color-space" : "extended-gray", 22 | "components" : { 23 | "alpha" : "1.000", 24 | "white" : "0.572" 25 | } 26 | }, 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/Assets.xcassets/buttonForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/Assets.xcassets/buttonPressedBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.745", 9 | "green" : "0.497", 10 | "red" : "0.497" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.745", 27 | "green" : "0.497", 28 | "red" : "0.497" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/Components/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ==----------------------------------------------------------------------== // 3 | // 4 | // ActivityIndicator.swift 5 | // 6 | // Created by Steven Grosmark on 03/26/2021 7 | // 8 | // 9 | // This source file is part of the Lasso open source project 10 | // 11 | // https://github.com/ww-tech/lasso 12 | // 13 | // Copyright © 2019-2021 WW International, Inc. 14 | // 15 | // ==----------------------------------------------------------------------== // 16 | // 17 | 18 | import UIKit 19 | import SwiftUI 20 | 21 | struct ActivityIndicator: View { 22 | 23 | @State var isAnimating: Bool = true 24 | 25 | var body: some View { 26 | if #available(iOS 14.0, *) { 27 | ProgressView().progressViewStyle(CircularProgressViewStyle()) 28 | } 29 | else { 30 | UIKitActivityIndicator(isAnimating: $isAnimating, style: .medium) 31 | } 32 | } 33 | 34 | private struct UIKitActivityIndicator: UIViewRepresentable { 35 | 36 | @Binding var isAnimating: Bool 37 | let style: UIActivityIndicatorView.Style 38 | 39 | func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView { 40 | let indicator = UIActivityIndicatorView(style: style) 41 | indicator.hidesWhenStopped = false 42 | return indicator 43 | } 44 | 45 | func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) { 46 | if isAnimating { 47 | uiView.startAnimating() 48 | } 49 | else { 50 | uiView.stopAnimating() 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/Lasso-SwiftUI/Lasso-SwiftUI/Components/NavigationRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ==----------------------------------------------------------------------== // 3 | // 4 | // NavigationRow.swift 5 | // 6 | // Created by Steven Grosmark on 03/24/2021. 7 | // 8 | // 9 | // This source file is part of the Lasso open source project 10 | // 11 | // https://github.com/ww-tech/lasso 12 | // 13 | // Copyright © 2019-2021 WW International, Inc. 14 | // 15 | // ==----------------------------------------------------------------------== // 16 | // 17 | 18 | import SwiftUI 19 | import Lasso 20 | 21 | /// A list item with a NavigationLink appearance, for dispatching a Lasso Action to a Store. 22 | struct NavigationRow