├── .gitignore ├── Previews ├── combine-zip.gif └── swiftui-96x96_2x.png ├── README.md ├── combine-extensions ├── CombineExtensions.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── CombineExtensions │ ├── CombineExtensions.h │ ├── CombineExtensions.swift │ ├── Info.plist │ └── UnwrapPublisher.swift └── CombineExtensionsTests │ ├── CombineExtensionsTests.swift │ └── Info.plist ├── combine-playground ├── combine-playground.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── combine-playground │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── ContentView │ ├── ContentView.swift │ └── ContentViewModel.swift │ ├── Data │ ├── DataModels.swift │ ├── DataService.swift │ ├── StreamStore.swift │ └── UserDefaultWrapper.swift │ ├── Extensions │ ├── ModelExtensions.swift │ └── OperatorExtensions.swift │ ├── Info.plist │ ├── Menu │ └── MenuRow.swift │ ├── MultiBallViews │ ├── MultiBallTunnelView.swift │ ├── MultiBallView.swift │ └── MultiBallViewModel.swift │ ├── MultiStreamViews │ ├── MultiStreamView.swift │ ├── MultiStreamViewModel.swift │ ├── MultiValueStreamView.swift │ └── UpdatableStreamViewModel.swift │ ├── PlaygroundStreamView │ ├── PlaygroundStreamView.swift │ └── PlaygroundStreamViewModel.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── SingleBallViews │ ├── BallTunnelView.swift │ ├── CircularTextView.swift │ └── CircularTextViewModel.swift │ ├── SingleStreamView │ ├── DataStreamViewModel.swift │ ├── SingleStreamView.swift │ └── StreamViewModel.swift │ ├── StreamListViews │ ├── JoinOperationListStreamView.swift │ ├── NewStreamView │ │ ├── NewStreamView.swift │ │ └── NewStreamViewModel.swift │ ├── OperationStreamListView.swift │ ├── StreamListView.swift │ └── UnifyingOperationListStreamView.swift │ ├── Styles │ ├── ButtonModifier.swift │ └── CombineDemoButton.swift │ ├── UpdateJoinStreamView │ ├── UpdateJoinStreamView.swift │ └── UpdateJoinStreamViewModel.swift │ ├── UpdateOperationStreamView │ ├── UpdateOperationStreamView.swift │ └── UpdateOperationStreamViewModel.swift │ ├── UpdateStreamView │ ├── UpdateStreamView.swift │ └── UpdateStreamViewModel.swift │ └── UpdateUnifyingStreamView │ ├── UpdateUnifyingStreamView.swift │ └── UpdateUnifyingStreamViewModel.swift └── tutorials └── combine-tutorial ├── chapter1 ├── CombineTutorial.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── CombineTutorial │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── CircularTextView.swift │ ├── CombineStreamView.swift │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── StreamView.swift │ └── TunnelView.swift ├── chapter2 ├── CombineTutorial.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── CombineTutorial │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── CircularTextView.swift │ ├── CombineMapStreamView.swift │ ├── CombineScanStreamView.swift │ ├── CombineStreamView.swift │ ├── ContentView.swift │ ├── GenericCombineStreamView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── StreamView.swift │ └── TunnelView.swift ├── chapter3 ├── CombineTutorial.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── CombineTutorial │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── CircularTextView.swift │ ├── CombineMapStreamView.swift │ ├── CombineScanStreamView.swift │ ├── CombineStreamView.swift │ ├── ContentView.swift │ ├── DoublePublisherStreamView.swift │ ├── GenericCombineStreamView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── StreamView.swift │ └── TunnelView.swift ├── chapter4 ├── CombineTutorial.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── CombineTutorial │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── CircularTextArrayView.swift │ ├── CircularTextView.swift │ ├── CombineMapStreamView.swift │ ├── CombineScanStreamView.swift │ ├── CombineStreamView.swift │ ├── ContentView.swift │ ├── DoublePublisherStreamView.swift │ ├── GenericCombineStreamView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── StreamView.swift │ └── TunnelView.swift └── chapter5 ├── CombineTutorial.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist └── CombineTutorial ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Base.lproj └── LaunchScreen.storyboard ├── ButtonModifier.swift ├── CircularTextArrayView.swift ├── CircularTextView.swift ├── CombineMapStreamView.swift ├── CombineScanStreamView.swift ├── CombineStreamView.swift ├── ContentView.swift ├── DescriptiveTunnelView.swift ├── DoublePublisherStreamView.swift ├── GenericCombineStreamView.swift ├── Info.plist ├── MultiCircularTextView.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json ├── SceneDelegate.swift ├── StreamView.swift └── TunnelView.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | # 51 | # Add this line if you want to avoid checking in source code from the Xcode workspace 52 | # *.xcworkspace 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # Accio dependency management 62 | Dependencies/ 63 | .accio/ 64 | 65 | # fastlane 66 | # 67 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 68 | # screenshots whenever they are needed. 69 | # For more information about the recommended setup visit: 70 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 71 | 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots/**/*.png 75 | fastlane/test_output 76 | 77 | # Code Injection 78 | # 79 | # After new code Injection tools there's a generated folder /iOSInjectionProject 80 | # https://github.com/johnno1962/injectionforxcode 81 | 82 | iOSInjectionProject/ 83 | .DS_Store 84 | -------------------------------------------------------------------------------- /Previews/combine-zip.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinjohnason/combine-magic-swiftui/6b4c9be67fcc423e4cfab1abf34512de3d4640b4/Previews/combine-zip.gif -------------------------------------------------------------------------------- /Previews/swiftui-96x96_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinjohnason/combine-magic-swiftui/6b4c9be67fcc423e4cfab1abf34512de3d4640b4/Previews/swiftui-96x96_2x.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## `Combine Magic with SwiftUI` 2 | 3 | ![quick demo](Previews/combine-zip.gif) 4 | 5 | ### About 6 | 7 | Visual examples using *SwiftUI* to demonstrate the behaviors of *Combine*. 8 | 9 | ### Combine Playground 10 | [SwiftUI App Source Code](combine-playground) 11 | 12 | ### Medium Series 13 | 1. Visualize Combine Magic with SwiftUI Series 14 | 1. [Create your Combine Playground in SwiftUI](https://medium.com/@kevinminority/visualize-combine-magic-with-swiftui-part-1-3a56e2a461b3) 15 | - [Source Code](tutorials/combine-tutorial/chapter1) 16 | 2. [Operators, subscribing, and canceling in Combine](https://medium.com/@kevinminority/visualize-combine-magic-with-swiftui-part-2-2c613370388b) 17 | - [Source Code](tutorials/combine-tutorial/chapter2) 18 | 3. [Merge and Append in Action](https://medium.com/@kevinminority/visualize-combine-magic-with-swiftui-part-3-a3f0cc42bcc8) 19 | - [Source Code](tutorials/combine-tutorial/chapter3) 20 | 4. [What are the differences between Zip and CombineLatest](https://medium.com/@kevinminority/visualize-combine-magic-with-swiftui-part-4-6d0c5678f89e) 21 | - [Source Code](tutorials/combine-tutorial/chapter4) 22 | 5. [SwiftUI ViewModifier, Animation, and Transition](https://medium.com/flawless-app-stories/visualize-combine-magic-with-swiftui-part-5-2783adddbd1d) 23 | - [Source Code](tutorials/combine-tutorial/chapter5) 24 | 2. Persist and Distribute Logic with Swift Combine 25 | 1. [Persist Business Logic With Swift Combine](https://medium.com/better-programming/persist-business-logic-with-swift-combine-519efb3a7e37) 26 | - [Source Code](combine-playground) 27 | 2. [Persist Filtering Logic With Swift Combine](https://medium.com/better-programming/persist-filtering-logics-with-swift-combine-6c3594be77cc) 28 | - [Source Code](combine-playground) 29 | -------------------------------------------------------------------------------- /combine-extensions/CombineExtensions.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /combine-extensions/CombineExtensions.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /combine-extensions/CombineExtensions/CombineExtensions.h: -------------------------------------------------------------------------------- 1 | // 2 | // CombineExtensions.h 3 | // CombineExtensions 4 | // 5 | // Created by kevin.cheng on 11/20/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for CombineExtensions. 12 | FOUNDATION_EXPORT double CombineExtensionsVersionNumber; 13 | 14 | //! Project version string for CombineExtensions. 15 | FOUNDATION_EXPORT const unsigned char CombineExtensionsVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /combine-extensions/CombineExtensions/CombineExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineExtensions.swift 3 | // combine-extensions 4 | // 5 | // Created by kevin.cheng on 11/20/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | public extension Set where Element == AnyCancellable { 13 | mutating func cancelAll() { 14 | self.forEach { 15 | $0.cancel() 16 | } 17 | self.removeAll() 18 | } 19 | } 20 | 21 | public typealias CancellableSet = Set 22 | -------------------------------------------------------------------------------- /combine-extensions/CombineExtensions/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /combine-extensions/CombineExtensions/UnwrapPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnwrapPublisher.swift 3 | // CombineExtensions 4 | // 5 | // Created by Kevin Cheng on 12/27/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import Combine 10 | 11 | public extension Publishers { 12 | 13 | struct Unwrap : Publisher where Upstream : Publisher, Upstream.Output == Optional { 14 | 15 | public typealias Failure = Upstream.Failure 16 | 17 | public let upstream: Upstream 18 | 19 | public init(upstream: Upstream) { 20 | self.upstream = upstream 21 | } 22 | 23 | public func receive(subscriber: Downstream) where Downstream : Subscriber, 24 | Failure == Downstream.Failure, Output == Downstream.Input { 25 | upstream.subscribe(UnwrapSubscriber(upstream: upstream, downstream: subscriber)) 26 | } 27 | } 28 | } 29 | 30 | extension Publishers.Unwrap { 31 | private class UnwrapSubscriber: Subscriber 32 | where Upstream.Failure == DownStream.Failure, Upstream.Output == Optional { 33 | 34 | private let upstream: Upstream 35 | private let downstream: DownStream 36 | 37 | init(upstream: Upstream, downstream: DownStream) { 38 | self.upstream = upstream 39 | self.downstream = downstream 40 | } 41 | 42 | func receive(subscription: Subscription) { 43 | downstream.receive(subscription: subscription) 44 | } 45 | 46 | func receive(_ input: Upstream.Output) -> Subscribers.Demand { 47 | guard let input = input else { 48 | return .none 49 | } 50 | return downstream.receive(input) 51 | } 52 | 53 | func receive(completion: Subscribers.Completion) { 54 | downstream.receive(completion: completion) 55 | } 56 | } 57 | } 58 | 59 | public extension Publisher { 60 | func unwrap() -> Publishers.Unwrap where Self.Output == Optional { 61 | Publishers.Unwrap(upstream: self) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /combine-extensions/CombineExtensionsTests/CombineExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineExtensionsTests.swift 3 | // CombineExtensionsTests 4 | // 5 | // Created by Kevin Cheng on 12/27/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Combine 11 | @testable import CombineExtensions 12 | 13 | class CombineExtensionsTests: XCTestCase { 14 | 15 | func testUnwrap() { 16 | let testValue = 5 17 | let testOptional: Int? = testValue 18 | let expectInt = expectation(description: "test value") 19 | _ = Just(testOptional).unwrap().sink { (result) in 20 | XCTAssertEqual(result, testValue) 21 | expectInt.fulfill() 22 | } 23 | let testArray: [Int?] = [1, nil, 3] 24 | let expectedArray = [1, 3] 25 | var resultArray: [Int] = [] 26 | let expectArray = expectation(description: "test sequence") 27 | _ = Publishers.Sequence(sequence: testArray) 28 | .unwrap() 29 | .sink { result in 30 | resultArray.append(result) 31 | if expectedArray == resultArray { 32 | expectArray.fulfill() 33 | } 34 | } 35 | wait(for: [expectInt, expectArray], timeout: 2) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /combine-extensions/CombineExtensionsTests/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /combine-playground/combine-playground.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /combine-playground/combine-playground.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // combine-playground 4 | // 5 | // Created by kevin.cheng on 11/7/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | func application(_ application: UIApplication, 14 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | // Override point for customization after application launch. 16 | return true 17 | } 18 | 19 | // MARK: UISceneSession Lifecycle 20 | 21 | func application(_ application: UIApplication, 22 | configurationForConnecting connectingSceneSession: UISceneSession, 23 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 25 | } 26 | 27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /combine-playground/combine-playground/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /combine-playground/combine-playground/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/ContentView/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // CombineDemo 4 | // 5 | // Created by Kevin Minority on 7/31/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | struct ContentView: View { 12 | 13 | var viewModel = ContentViewModel() 14 | 15 | var body: some View { 16 | NavigationView { 17 | VStack { 18 | List { 19 | Section(header: Text("Stream data")) { 20 | StreamListView() 21 | } 22 | Section(header: Text("Basic Operators")) { 23 | OperationStreamListView() 24 | } 25 | Section(header: Text("Unifying Operators")) { 26 | UnifyingOperationListStreamView() 27 | } 28 | Section(header: Text("Join Operators")) { 29 | JoinOperationListStreamView() 30 | } 31 | Section(header: Text("Playground")) { 32 | NavigationLink(destination: PlaygroundStreamView()) { 33 | MenuRow(detailViewName: "Playground") 34 | } 35 | } 36 | } 37 | Button("Reset") { 38 | DataService.shared.resetStoredStream() 39 | }.frame(maxWidth: .infinity, maxHeight: 25) 40 | .modifier(DemoButton(backgroundColor: .red)) 41 | }.navigationBarTitle("Streams") 42 | .navigationBarItems(leading: EditButton(), trailing: createStreamView) 43 | } 44 | } 45 | 46 | var createStreamView: some View { 47 | NavigationLink(destination: NewStreamView(viewModel: viewModel.newStreamViewModel) ) { 48 | Image(systemName: "plus.circle").font(Font.system(size: 30)) 49 | } 50 | } 51 | } 52 | 53 | #if DEBUG 54 | struct ContentView_Previews: PreviewProvider { 55 | static var previews: some View { 56 | ContentView()//.previewDevice(PreviewDevice(rawValue: "iPad Pro (9.7-inch)")) 57 | } 58 | } 59 | #endif 60 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/ContentView/ContentViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentViewModel.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 8/30/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | class ContentViewModel: ObservableObject { 13 | 14 | private var disposables = Set() 15 | 16 | var cancellable: Cancellable? 17 | 18 | lazy var newStreamViewModel = NewStreamViewModel() 19 | 20 | init() { 21 | refresh() 22 | } 23 | 24 | func refresh() { 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/Data/UserDefaultWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsWrapper.swift 3 | // combine-playground 4 | // 5 | // Created by Kevin Cheng on 3/8/20. 6 | // Copyright © 2020 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @propertyWrapper 12 | struct UserDefault { 13 | let key: String 14 | let defaultValue: T 15 | var wrappedValue: T { 16 | get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } 17 | set { UserDefaults.standard.set(newValue, forKey: key) } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/Extensions/ModelExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineService.swift 3 | // CombineDemo 4 | // 5 | // Created by Kevin Minority on 7/31/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | extension StreamModel { 13 | func toArrayStreamModel() -> StreamModel<[T]> { 14 | StreamModel<[T]>.init(id: self.id, name: self.name, description: self.description, 15 | stream: self.stream.map { StreamItem(value: [$0.value], operators: $0.operators) }, 16 | isDefault: self.isDefault) 17 | } 18 | } 19 | 20 | extension StreamModel where T == [String] { 21 | func flatMapModel() -> StreamModel { 22 | StreamModel(id: self.id, name: self.name, description: self.description, 23 | stream: self.stream.map { StreamItem(value: $0.value[0], 24 | operators: $0.operators) }, 25 | isDefault: self.isDefault) 26 | } 27 | } 28 | 29 | extension StreamModel where T == String { 30 | 31 | var sequenceDescription: String { 32 | var desc = self.stream.reduce("Sequence(") { 33 | "\($0)\($1.value), " 34 | } 35 | guard let finalDotIndex = desc.lastIndex(of: ",") else { 36 | return "Empty()" 37 | } 38 | desc.removeSubrange(finalDotIndex.. StreamModel { 44 | let streamItems = self.stream.map { StreamItem(value: Int($0.value) ?? 0, operators: $0.operators) } 45 | return StreamModel(id: id, name: name, description: description, 46 | stream: streamItems, isDefault: isDefault) 47 | } 48 | } 49 | 50 | extension StreamModel { 51 | 52 | func toPublisher() -> AnyPublisher { 53 | let intervalPublishers = 54 | self.stream.map { $0.toPublisher() } 55 | 56 | guard intervalPublishers.count > 0 else { 57 | return Empty().eraseToAnyPublisher() 58 | } 59 | return intervalPublishers[1...].reduce(intervalPublishers[0]) { 60 | $0.append($1).eraseToAnyPublisher() 61 | } 62 | } 63 | } 64 | 65 | extension StreamItem { 66 | func toPublisher() -> AnyPublisher { 67 | var publisher: AnyPublisher = Just(value).eraseToAnyPublisher() 68 | self.operators.forEach { 69 | publisher = $0.applyPublisher(publisher) 70 | } 71 | return publisher 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/Menu/MenuRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuRow.swift 3 | // CombineDemo 4 | // 5 | // Created by Kevin Minority on 7/31/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MenuRow: View { 12 | let detailViewName: String 13 | var body: some View { 14 | Text(detailViewName) 15 | } 16 | } 17 | 18 | #if DEBUG 19 | struct MenuRow_Previews: PreviewProvider { 20 | static var previews: some View { 21 | MenuRow(detailViewName: "Single Stream") 22 | .previewLayout(.fixed(width: 300, height: 70)) 23 | } 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/MultiBallViews/MultiBallTunnelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiBallTunnelView.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 9/12/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MultiBallTunnelView: View { 12 | @Binding var values: [IdentifiableValue<[String]>] 13 | var color: Color = .green 14 | var animationSecond: Double = 2 15 | 16 | var ballRadius: CGFloat = 49 17 | 18 | var body: some View { 19 | GeometryReader { tunnelGeometry in 20 | HStack(spacing: 0) { 21 | Spacer() 22 | ForEach(self.values.reversed()) { value in 23 | MultiBallView(forgroundColor: .white, backgroundColor: self.color, 24 | viewModel: .constant(MultiBallViewModel(values: value.value))) 25 | .frame(width: self.ballRadius * CGFloat(value.value.count), 26 | height: self.ballRadius, alignment: .center) 27 | .transition(.asymmetric(insertion: 28 | .offset(x: -tunnelGeometry.size.width, y: 0), 29 | removal: .offset(x: tunnelGeometry.size.width, y: 0))) 30 | } 31 | } 32 | .frame(minWidth: max(tunnelGeometry.size.width, 33 | self.ballRadius * 2 * CGFloat(self.values.count)), 34 | minHeight: self.ballRadius, alignment: .trailing) 35 | .padding([.top, .bottom], 5) 36 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .trailing) 37 | .background(Color(red: 242/255.0, green: 242/255.0, blue: 242/255.0)) 38 | .animation(.easeInOut(duration: self.animationSecond)) 39 | } 40 | } 41 | } 42 | 43 | struct MultiBallTunnelView_Previews: PreviewProvider { 44 | static var previews: some View { 45 | MultiBallTunnelView(values: .constant([IdentifiableValue(value: ["14"])])) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/MultiBallViews/MultiBallView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiBallView.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 9/12/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MultiBallView: View { 12 | var forgroundColor: Color 13 | var backgroundColor: Color 14 | @Binding var viewModel: MultiBallViewModel 15 | 16 | var body: some View { 17 | HStack(spacing: 0) { 18 | ForEach(viewModel.values, id: \.self) { value in 19 | CircularTextView(forgroundColor: self.forgroundColor, 20 | backgroundColor: self.backgroundColor, viewModel: CircularTextViewModel(value: value)) 21 | } 22 | } 23 | } 24 | } 25 | 26 | struct MultiBallView_Previews: PreviewProvider { 27 | static var previews: some View { 28 | MultiBallView(forgroundColor: .white, backgroundColor: .red, 29 | viewModel: .constant(MultiBallViewModel(values: ["14"]))) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/MultiBallViews/MultiBallViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiBallViewModel.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 9/12/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | class MultiBallViewModel: ObservableObject, Identifiable { 12 | @Published var values: [String] 13 | // swiftlint:disable identifier_name 14 | let id: Date = Date() 15 | @Published var isHidden: Bool = false 16 | @Published var offset: CGSize = .zero 17 | init(values: [String]) { 18 | self.values = values 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/MultiStreamViews/MultiStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiStreamView.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 9/12/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | import CombineExtensions 12 | struct MultiStreamView: View { 13 | @ObservedObject var viewModel: MultiStreamViewModel 14 | @EnvironmentObject var streamStore: StreamStore 15 | 16 | func trailingBarItem() -> some View { 17 | guard viewModel.operationStreamModel != nil, 18 | let operationStreamViewModel = viewModel.addOperationStreamViewModel else { 19 | return AnyView(EmptyView()) 20 | } 21 | let navigationLink = NavigationLink( 22 | destination: UpdateOperationStreamView( 23 | viewModel: operationStreamViewModel)) { 24 | Image(systemName: "plus.circle").font(Font.system(size: 30)) 25 | } 26 | return AnyView(navigationLink) 27 | } 28 | 29 | var body: some View { 30 | VStack { 31 | ForEach(viewModel.streamViewModels, id: \.title) { streamViewModel in 32 | MultiValueStreamView(viewModel: streamViewModel, 33 | displayActionButtons: false) 34 | } 35 | HStack { 36 | CombineDemoButton(text: "Subscribe", backgroundColor: .blue) { 37 | self.viewModel.streamViewModels.forEach { 38 | $0.subscribe() 39 | } 40 | } 41 | CombineDemoButton(text: "Cancel", backgroundColor: .red) { 42 | self.viewModel.streamViewModels.forEach { 43 | $0.cancel() 44 | } 45 | } 46 | }.padding() 47 | }.navigationBarTitle(viewModel.title) 48 | .navigationBarItems(trailing: self.trailingBarItem()) 49 | } 50 | } 51 | 52 | //struct MultiStreamView_Previews: PreviewProvider { 53 | // static var previews: some View { 54 | // MultiStreamView(streamTitle: "", 55 | // sourceStreamModel: StreamModel.new(), 56 | // operationStreamModel: .delay(seconds: 1, next: nil)) 57 | // } 58 | //} 59 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/MultiStreamViews/MultiValueStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineStreamView.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 8/7/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | struct MultiValueStreamView: View { 12 | @ObservedObject var viewModel: StreamViewModel<[String]> 13 | 14 | var displayActionButtons: Bool = true 15 | 16 | var updateView: AnyView? { 17 | guard let updtableStreamViewModel = viewModel as? UpdatableStreamViewModel else { 18 | return nil 19 | } 20 | if let updateOperationStreamViewModel = updtableStreamViewModel.updateOperationStreamViewModel { 21 | return AnyView(UpdateOperationStreamView(viewModel: updateOperationStreamViewModel)) 22 | } else if let updateUnifyingStreamViewModel = updtableStreamViewModel.updateUnifyingStreamViewModel { 23 | return AnyView(UpdateUnifyingStreamView(viewModel: updateUnifyingStreamViewModel)) 24 | } else if let updateJoinStreamViewModel = updtableStreamViewModel.updateJoinStreamViewModel { 25 | return AnyView(UpdateJoinStreamView(viewModel: updateJoinStreamViewModel)) 26 | } else { 27 | return nil 28 | } 29 | } 30 | 31 | var navigationView: some View { 32 | guard let updateView = updateView else { 33 | return AnyView(EmptyView()) 34 | } 35 | return AnyView(NavigationLink( 36 | destination: updateView, 37 | label: { 38 | HStack { 39 | Spacer() 40 | Image(systemName: "pencil.circle") 41 | .font(.system(size: 25, weight: .light)) 42 | }.padding(.trailing, 10) 43 | })) 44 | } 45 | 46 | var body: some View { 47 | VStack(spacing: 10) { 48 | Spacer() 49 | ZStack { 50 | Text(viewModel.title) 51 | .font(.system(.headline, design: .monospaced)) 52 | .lineLimit(nil) 53 | self.navigationView 54 | } 55 | 56 | Text(viewModel.updatableDescription) 57 | .font(.system(.subheadline, design: .monospaced)) 58 | .lineLimit(nil) 59 | 60 | MultiBallTunnelView(values: $viewModel.values, color: .green, 61 | animationSecond: viewModel.animationSeconds).frame(maxHeight: 100) 62 | 63 | if displayActionButtons { 64 | HStack { 65 | CombineDemoButton(text: "Subscribe", backgroundColor: .blue) { 66 | self.viewModel.subscribe() 67 | } 68 | 69 | CombineDemoButton(text: "Cancel", backgroundColor: .red) { 70 | self.viewModel.cancel() 71 | } 72 | }.padding() 73 | } 74 | Spacer() 75 | } 76 | } 77 | } 78 | 79 | #if DEBUG 80 | struct CombineSingleStreamView_Previews: PreviewProvider { 81 | static var previews: some View { 82 | MultiValueStreamView(viewModel: StreamViewModel<[String]>(title: "Stream A", 83 | description: "Sequence(A,B,C,D)", 84 | publisher: Empty().eraseToAnyPublisher())) 85 | } 86 | } 87 | #endif 88 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/PlaygroundStreamView/PlaygroundStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaygroundStreamView.swift 3 | // combine-playground 4 | // 5 | // Created by Kevin Cheng on 12/31/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct PlaygroundStreamView: View { 12 | @EnvironmentObject var streamStore: StreamStore 13 | 14 | var viewModel: PlaygroundStreamViewModel { 15 | PlaygroundStreamViewModel(streamStore: streamStore) 16 | } 17 | 18 | var body: some View { 19 | VStack { 20 | MultiStreamView(viewModel: viewModel.multiStreamViewModel) 21 | } 22 | } 23 | } 24 | 25 | struct PlaygroundStreamView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | PlaygroundStreamView() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/PlaygroundStreamView/PlaygroundStreamViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaygroundViewModel.swift 3 | // combine-playground 4 | // 5 | // Created by Kevin Cheng on 12/31/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | class PlaygroundStreamViewModel: ObservableObject { 12 | var multiStreamViewModel: MultiStreamViewModel 13 | 14 | let streamStore: StreamStore 15 | 16 | init(streamStore: StreamStore) { 17 | self.streamStore = streamStore 18 | 19 | let streamA = (1...4).map { StreamItem(value: $0, 20 | operators: [.delay(seconds: 1)]) } 21 | let serialStreamA = StreamModel(id: UUID(), name: "Serial Stream A", 22 | description: nil, stream: streamA, isDefault: true) 23 | 24 | let publisher = serialStreamA.toPublisher() 25 | let sourceStreamViewModel = StreamViewModel(title: "Source Stream", publisher: publisher) 26 | 27 | let scanPublisher = TransformingOperator.scan(initialValue: 0, expression: "%d + %d").applyPublisher(publisher) 28 | let scanStreamViewModel = StreamViewModel(title: "Scan Result", 29 | description: ".scan(0) { %d + %d }", publisher: scanPublisher) 30 | let mapPublisher = TransformingOperator.map(expression: "%d * 2").applyPublisher(scanPublisher) 31 | let mapStreamViewModel = StreamViewModel(title: "Map Result", 32 | description: ".map { %d * 2 }", publisher: mapPublisher) 33 | multiStreamViewModel = MultiStreamViewModel(title: "Playground", 34 | streamViewModels: [sourceStreamViewModel, 35 | scanStreamViewModel, mapStreamViewModel]) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /combine-playground/combine-playground/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // combine-playground 4 | // 5 | // Created by kevin.cheng on 11/7/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, 17 | options connectionOptions: UIScene.ConnectionOptions) { 18 | let contentView = ContentView().environmentObject(StreamStore()) 19 | 20 | // Use a UIHostingController as window root view controller. 21 | if let windowScene = scene as? UIWindowScene { 22 | let window = UIWindow(windowScene: windowScene) 23 | window.rootViewController = UIHostingController(rootView: contentView) 24 | self.window = window 25 | window.makeKeyAndVisible() 26 | } 27 | } 28 | 29 | func sceneDidDisconnect(_ scene: UIScene) { 30 | 31 | } 32 | 33 | func sceneDidBecomeActive(_ scene: UIScene) { 34 | // Called when the scene has moved from an inactive state to an active state. 35 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 36 | } 37 | 38 | func sceneWillResignActive(_ scene: UIScene) { 39 | // Called when the scene will move from an active state to an inactive state. 40 | // This may occur due to temporary interruptions (ex. an incoming phone call). 41 | } 42 | 43 | func sceneWillEnterForeground(_ scene: UIScene) { 44 | // Called as the scene transitions from the background to the foreground. 45 | // Use this method to undo the changes made on entering the background. 46 | } 47 | 48 | func sceneDidEnterBackground(_ scene: UIScene) { 49 | // Called as the scene transitions from the foreground to the background. 50 | // Use this method to save data, release shared resources, and store enough scene-specific state information 51 | // to restore the scene back to its current state. 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/SingleBallViews/BallTunnelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BallTunnelView.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 8/2/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct BoundsPreferenceData { 12 | let bounds: Anchor? 13 | } 14 | 15 | struct TunnelPreferenceKey: PreferenceKey { 16 | static var defaultValue: BoundsPreferenceData = BoundsPreferenceData(bounds: nil) 17 | 18 | static func reduce(value: inout BoundsPreferenceData, nextValue: () -> BoundsPreferenceData) { 19 | value = nextValue() 20 | } 21 | typealias Value = BoundsPreferenceData 22 | } 23 | 24 | struct BallTunnelView: View { 25 | @Binding var values: [IdentifiableValue] 26 | var color: Color = .green 27 | 28 | var animationSecond: Double = 2 29 | 30 | var ballRadius: CGFloat = 48 31 | 32 | var padding: CGFloat = 5 33 | 34 | let tunnelColor: Color = Color(red: 242/255.0, green: 242/255.0, blue: 242/255.0) 35 | 36 | var body: some View { 37 | GeometryReader { tunnelGeometry in 38 | HStack(spacing: 0) { 39 | ForEach(self.values.reversed()) { value in 40 | CircularTextView(forgroundColor: .white, backgroundColor: self.color, 41 | viewModel: CircularTextViewModel(value: value.value)) 42 | .frame(width: self.ballRadius, height: self.ballRadius, alignment: .center) 43 | .transition(.asymmetric(insertion: .offset(x: -tunnelGeometry.size.width, y: 0), 44 | removal: .offset(x: tunnelGeometry.size.width, y: 0))) 45 | } 46 | } 47 | .frame(minWidth: self.tunnelWidth(with: tunnelGeometry.size.width), 48 | minHeight: self.ballRadius, alignment: .trailing) 49 | .offset(x: self.tunnelOffset(with: tunnelGeometry.size.width)) 50 | .padding([.top, .bottom], self.padding) 51 | .background(self.tunnelColor) 52 | .anchorPreference(key: TunnelPreferenceKey.self, value: .bounds, transform: { 53 | BoundsPreferenceData(bounds: $0) 54 | }).animation(.easeInOut(duration: self.animationSecond)) 55 | } 56 | } 57 | 58 | func tunnelWidth(with screenWidth: CGFloat) -> CGFloat { 59 | max(screenWidth, self.ballRadius * CGFloat(self.values.count)) 60 | } 61 | 62 | func tunnelOffset(with screenWidth: CGFloat) -> CGFloat { 63 | (tunnelWidth(with: screenWidth) - screenWidth) / 2 64 | } 65 | } 66 | 67 | #if DEBUG 68 | struct BallTunnelView_Previews: PreviewProvider { 69 | static var previews: some View { 70 | BallTunnelView(values: .constant([])) 71 | } 72 | } 73 | #endif 74 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/SingleBallViews/CircularTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BallView.swift 3 | // CombineDemo 4 | // 5 | // Created by Kevin Minority on 7/31/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CircularTextView: View { 12 | var forgroundColor: Color 13 | var backgroundColor: Color 14 | var draggable: Bool = false 15 | @ObservedObject var viewModel: CircularTextViewModel 16 | 17 | var body: some View { 18 | Text(self.viewModel.value) 19 | .font(.system(size: 14)) 20 | .bold() 21 | .foregroundColor(self.forgroundColor) 22 | .padding() 23 | .background(self.backgroundColor) 24 | .clipShape(Circle()) 25 | .shadow(radius: 1) 26 | .offset(self.viewModel.offset) 27 | .opacity(viewModel.isHidden ? 0 : 1) 28 | } 29 | } 30 | 31 | #if DEBUG 32 | struct BallView_Previews: PreviewProvider { 33 | static var previews: some View { 34 | CircularTextView(forgroundColor: .white, backgroundColor: .red, 35 | viewModel: CircularTextViewModel(value: "")) 36 | } 37 | } 38 | #endif 39 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/SingleBallViews/CircularTextViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BallViewModel.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 8/27/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | class CircularTextViewModel: ObservableObject, Identifiable { 13 | @Published var value: String 14 | // swiftlint:disable identifier_name 15 | let id: Date = Date() 16 | @Published var isHidden: Bool = false 17 | @Published var offset: CGSize = .zero 18 | init(value: String) { 19 | self.value = value 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/SingleStreamView/DataStreamViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicStreamViewModel.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 8/28/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | class DataStreamViewModel: StreamViewModel { 12 | 13 | var streamModel: StreamModel { 14 | didSet { 15 | self.title = self.streamModel.name ?? "" 16 | self.description = self.streamModel.description ?? "" 17 | self.publisher = self.streamModel.toPublisher() 18 | } 19 | } 20 | 21 | lazy var updateStreamViewModel: UpdateStreamViewModel = 22 | UpdateStreamViewModel(streamModel: self.streamModel) 23 | 24 | init(streamModel: StreamModel) { 25 | self.streamModel = streamModel 26 | super.init(title: streamModel.name ?? "", 27 | description: streamModel.description ?? streamModel.sequenceDescription, 28 | publisher: streamModel.toPublisher(), editable: true) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/SingleStreamView/SingleStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleStreamView.swift 3 | // CombineDemo 4 | // 5 | // Created by Kevin Minority on 7/31/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct SingleStreamView: View { 13 | 14 | @ObservedObject var viewModel: StreamViewModel 15 | 16 | @EnvironmentObject var streamStore: StreamStore 17 | 18 | var color: Color = .green 19 | 20 | var displayActionButtons: Bool = true 21 | 22 | var body: some View { 23 | VStack(spacing: 10) { 24 | Spacer() 25 | Text(viewModel.updatableDescription) 26 | .font(.system(.headline, design: .monospaced)) 27 | .lineLimit(nil).padding() 28 | BallTunnelView(values: $viewModel.values, color: color, 29 | animationSecond: viewModel.animationSeconds) 30 | .frame(maxHeight: 100) 31 | if displayActionButtons { 32 | HStack { 33 | CombineDemoButton(text: "Subscribe", backgroundColor: .blue) { 34 | self.viewModel.subscribe() 35 | } 36 | CombineDemoButton(text: "Cancel", backgroundColor: .red) { 37 | self.viewModel.cancel() 38 | } 39 | }.padding() 40 | } 41 | Spacer() 42 | }.navigationBarTitle(viewModel.updatableTitle) 43 | .navigationBarItems(trailing: trailingBarItem) 44 | } 45 | 46 | var trailingBarItem: some View { 47 | guard let dataStreamViewModel = viewModel as? DataStreamViewModel else { 48 | return AnyView(EmptyView()) 49 | } 50 | let navigationLink = NavigationLink(destination: UpdateStreamView(viewModel: 51 | dataStreamViewModel.updateStreamViewModel)) { 52 | Text("Edit") 53 | } 54 | return AnyView(navigationLink) 55 | } 56 | } 57 | 58 | #if DEBUG 59 | struct SingleStreamView_Previews: PreviewProvider { 60 | static var previews: some View { 61 | SingleStreamView(viewModel: StreamViewModel(title: "Stream A", 62 | description: "Sequence(1,2,3,4,5)", 63 | publisher: Empty().eraseToAnyPublisher())) 64 | } 65 | } 66 | #endif 67 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/SingleStreamView/StreamViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleStreamViewModel.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 8/1/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import Combine 12 | 13 | class StreamViewModel: ObservableObject { 14 | 15 | var title: String { 16 | didSet { 17 | updatableTitle = title 18 | } 19 | } 20 | // Simply to work around XCode bug that doesn't comipled a publishable title 21 | @Published var updatableTitle: String 22 | var description: String { 23 | didSet { 24 | updatableDescription = description 25 | } 26 | } 27 | // Simply to work around XCode bug that doesn't comipled a publishable description 28 | @Published var updatableDescription: String 29 | var publisher: AnyPublisher 30 | @Published var values: [IdentifiableValue] = [] 31 | let animationSeconds: Double = 1.5 32 | var cancellable: Cancellable? 33 | var editable: Bool 34 | 35 | init(title: String, description: String = "", publisher: AnyPublisher, editable: Bool = false) { 36 | self.title = title 37 | self.updatableTitle = title 38 | self.description = description 39 | self.updatableDescription = description 40 | self.publisher = publisher 41 | self.editable = editable 42 | } 43 | 44 | func subscribe() { 45 | cancellable?.cancel() 46 | cancellable = publisher.map { value in 47 | var newValues = self.values 48 | newValues.append(IdentifiableValue(value: value)) 49 | return newValues 50 | }.assign(to: \.values, on: self) 51 | } 52 | 53 | func cancel() { 54 | self.cancellable?.cancel() 55 | self.values.removeAll() 56 | } 57 | 58 | deinit { 59 | cancellable?.cancel() 60 | } 61 | 62 | func toArrayViewModel() -> StreamViewModel<[T]> { 63 | StreamViewModel<[T]>(title: self.title, description: self.description, 64 | publisher: self.publisher.map { [$0] }.eraseToAnyPublisher(), editable: self.editable) 65 | } 66 | 67 | func toStringArrayViewModel() -> StreamViewModel<[String]> { 68 | StreamViewModel<[String]>(title: self.title, description: self.description, 69 | publisher: self.publisher.map { ["\($0)"] }.eraseToAnyPublisher(), editable: self.editable) 70 | } 71 | } 72 | 73 | struct IdentifiableValue: Identifiable { 74 | var value: T 75 | // swiftlint:disable identifier_name 76 | var id: UUID 77 | 78 | init(value: T) { 79 | self.id = UUID() 80 | self.value = value 81 | } 82 | } 83 | 84 | extension StreamViewModel where T == [String] { 85 | func flatMapViewModel() -> StreamViewModel { 86 | StreamViewModel(title: self.title, description: self.description, 87 | publisher: self.publisher.map { $0[0] }.eraseToAnyPublisher(), editable: self.editable) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/StreamListViews/JoinOperationListStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineGroupOperationListStreamView.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 9/6/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct JoinOperationListStreamView: View { 12 | 13 | @EnvironmentObject var streamStore: StreamStore 14 | 15 | func streamView(streamModel: JoinOperationStreamModel) -> some View { 16 | let sourceStreams = streamStore.streams.filter { $0.isDefault } 17 | guard sourceStreams.count > 1 else { 18 | return AnyView(EmptyView()) 19 | } 20 | let viewModel = MultiStreamViewModel(streamTitle: streamModel.name ?? "", 21 | stream1Model: sourceStreams[0], 22 | stream2Model: sourceStreams[1], 23 | joinStreamModel: streamModel) 24 | let operationStreamView = MultiStreamView(viewModel: viewModel) 25 | return AnyView(operationStreamView) 26 | } 27 | 28 | var body: some View { 29 | ForEach(streamStore.joinStreams) { stream in 30 | NavigationLink(destination: self.streamView(streamModel: stream)) { 31 | MenuRow(detailViewName: stream.name ?? "") 32 | } 33 | }.onMove { (source, destination) in 34 | var storedStreams = self.streamStore.joinStreams 35 | storedStreams.move(fromOffsets: source, toOffset: destination) 36 | self.streamStore.joinStreams = storedStreams 37 | } 38 | } 39 | } 40 | 41 | struct JoinOperationListStreamView_Previews: PreviewProvider { 42 | static var previews: some View { 43 | JoinOperationListStreamView() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/StreamListViews/NewStreamView/NewStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewStreamView.swift 3 | // combine-playground 4 | // 5 | // Created by kevin.cheng on 12/26/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct NewStreamView: View { 12 | 13 | @ObservedObject var viewModel: NewStreamViewModel 14 | 15 | var body: some View { 16 | VStack(alignment: .center, spacing: 10) { 17 | Picker(selection: self.$viewModel.selectedTitle, label: Text("Select a Type").font(.footnote)) { 18 | ForEach(self.viewModel.streamTitles, id: \.self) { streamType in 19 | Text(streamType) 20 | } 21 | }.padding() 22 | 23 | if viewModel.selectedTitle == viewModel.streamTitles[1] { 24 | UpdateOperationStreamView(viewModel: viewModel.newOperationStreamViewModel) 25 | } else if viewModel.selectedTitle == viewModel.streamTitles[2] { 26 | UpdateUnifyingStreamView(viewModel: viewModel.newUnifyingStreamViewModel) 27 | } else if viewModel.selectedTitle == viewModel.streamTitles[3] { 28 | UpdateJoinStreamView(viewModel: viewModel.newJoinStreamViewModel) 29 | } else { 30 | UpdateStreamView(viewModel: viewModel.newStreamViewModel) 31 | } 32 | } 33 | } 34 | } 35 | 36 | //struct NewStreamView_Previews: PreviewProvider { 37 | // static var previews: some View { 38 | // AddNewStreamView() 39 | // } 40 | //} 41 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/StreamListViews/NewStreamView/NewStreamViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewStreamViewModel.swift 3 | // combine-playground 4 | // 5 | // Created by kevin.cheng on 12/26/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | import CombineExtensions 12 | 13 | class NewStreamViewModel: ObservableObject { 14 | let streamTitles: [String] = ["Stream Source", "Basic Operator", "Unifying Operator", "Join Operator"] 15 | @Published var selectedTitle: String = "" 16 | private var disposebles = CancellableSet() 17 | 18 | lazy var newStreamViewModel: UpdateStreamViewModel = { 19 | UpdateStreamViewModel(streamModel: StreamModel.new()) 20 | }() 21 | 22 | lazy var newOperationStreamViewModel: UpdateOperationStreamViewModel = { 23 | UpdateOperationStreamViewModel(sourceStreamModel: StreamModel.new()) 24 | }() 25 | 26 | lazy var newUnifyingStreamViewModel: UpdateUnifyingStreamViewModel = { 27 | UpdateUnifyingStreamViewModel(sourceStreamModels: [StreamModel.new(), StreamModel.new()]) 28 | }() 29 | 30 | lazy var newJoinStreamViewModel: UpdateJoinStreamViewModel = { 31 | UpdateJoinStreamViewModel(sourceStreamModels: [StreamModel.new(), StreamModel.new()]) 32 | }() 33 | } 34 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/StreamListViews/OperationStreamListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationStreamListView.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 9/6/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct OperationStreamListView: View { 12 | @EnvironmentObject var streamStore: StreamStore 13 | 14 | func streamView(streamModel: OperationStreamModel) -> some View { 15 | let multiStreamViewModel = MultiStreamViewModel(title: streamModel.name ?? "", 16 | sourceStreamModel: streamStore.streamAModel, 17 | operationStreamModel: streamModel) 18 | return MultiStreamView(viewModel: multiStreamViewModel) 19 | } 20 | 21 | var body: some View { 22 | ForEach(streamStore.operationStreams) { stream in 23 | NavigationLink(destination: self.streamView(streamModel: stream)) { 24 | MenuRow(detailViewName: stream.name ?? "") 25 | } 26 | }.onMove { (source, destination) in 27 | var storedStreams = self.streamStore.operationStreams 28 | storedStreams.move(fromOffsets: source, toOffset: destination) 29 | self.streamStore.operationStreams = storedStreams 30 | } 31 | } 32 | } 33 | 34 | struct OperationStreamListView_Previews: PreviewProvider { 35 | static var previews: some View { 36 | OperationStreamListView() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/StreamListViews/StreamListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StreamListView.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 8/30/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct StreamListView: View { 12 | 13 | @EnvironmentObject var streamStore: StreamStore 14 | 15 | @State var deleteAlertInDisplay: Bool = false 16 | 17 | func streamView(streamModel: StreamModel) -> some View { 18 | return AnyView(SingleStreamView(viewModel: DataStreamViewModel(streamModel: streamModel))) 19 | } 20 | 21 | var body: some View { 22 | ForEach(streamStore.streams) { stream in 23 | NavigationLink(destination: self.streamView(streamModel: stream)) { 24 | MenuRow(detailViewName: stream.name ?? "") 25 | } 26 | }.onDelete { (index) in 27 | guard let removingIndex = index.first else { 28 | return 29 | } 30 | if self.streamStore.streams[removingIndex].isDefault { 31 | self.deleteAlertInDisplay = true 32 | return 33 | } 34 | self.streamStore.streams.remove(at: removingIndex) 35 | }.onMove { (source, destination) in 36 | var storedStreams = self.streamStore.streams 37 | storedStreams.move(fromOffsets: source, toOffset: destination) 38 | self.streamStore.streams = storedStreams 39 | }.alert(isPresented: $deleteAlertInDisplay) { () -> Alert in 40 | Alert(title: Text("Don't do that"), 41 | message: Text("You can't delete default streams"), dismissButton: .cancel()) 42 | } 43 | } 44 | } 45 | 46 | struct StreamListView_Previews: PreviewProvider { 47 | static var previews: some View { 48 | StreamListView().environmentObject(StreamStore()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/StreamListViews/UnifyingOperationListStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupOperationListStreamView.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 9/6/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct UnifyingOperationListStreamView: View { 12 | @EnvironmentObject var streamStore: StreamStore 13 | func streamView(streamModel: UnifyingOperationStreamModel) -> some View { 14 | let sourceStreams = streamStore.streams.filter { $0.isDefault } 15 | guard sourceStreams.count > 1 else { 16 | return AnyView(EmptyView()) 17 | } 18 | let operationStreamViewModel = MultiStreamViewModel(streamTitle: streamModel.name ?? "", 19 | stream1Model: sourceStreams[0], 20 | stream2Model: sourceStreams[1], 21 | unifyingStreamModel: streamModel) 22 | return AnyView(MultiStreamView(viewModel: operationStreamViewModel)) 23 | } 24 | 25 | var body: some View { 26 | ForEach(streamStore.unifyingStreams) { stream in 27 | NavigationLink(destination: self.streamView(streamModel: stream)) { 28 | MenuRow(detailViewName: stream.name ?? "") 29 | } 30 | }.onMove { (source, destination) in 31 | var storedStreams = self.streamStore.unifyingStreams 32 | storedStreams.move(fromOffsets: source, toOffset: destination) 33 | self.streamStore.unifyingStreams = storedStreams 34 | } 35 | } 36 | } 37 | 38 | //struct GroupOperationListStreamView_Previews: PreviewProvider { 39 | // static var previews: some View { 40 | // UnifyingOperationListStreamView(storedUnifyingOperationStreams: .constant([])) 41 | // } 42 | //} 43 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/Styles/ButtonModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonModifier.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 8/30/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct DemoButton: ViewModifier { 12 | 13 | let backgroundColor: Color 14 | 15 | func body(content: Content) -> some View { 16 | content 17 | .font(.footnote) 18 | .padding(10) 19 | .foregroundColor(Color.white) 20 | .frame(minWidth: 80) 21 | .background(backgroundColor) 22 | .cornerRadius(12) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/Styles/CombineDemoButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineDemoButton.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 8/5/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CombineDemoButton: View { 12 | let text: String 13 | let backgroundColor: Color 14 | let buttonAction: () -> Void 15 | var body: some View { 16 | Button(text, action: buttonAction) 17 | .modifier(DemoButton(backgroundColor: backgroundColor)) 18 | } 19 | } 20 | 21 | #if DEBUG 22 | struct CombineDemoButton_Previews: PreviewProvider { 23 | static var previews: some View { 24 | CombineDemoButton(text: "T", backgroundColor: .blue) { 25 | } 26 | } 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/UpdateJoinStreamView/UpdateJoinStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateJoinStreamView.swift 3 | // combine-playground 4 | // 5 | // Created by kevin.cheng on 11/20/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | struct UpdateJoinStreamView: View { 12 | 13 | @Environment(\.presentationMode) var presentationMode: Binding 14 | @ObservedObject var viewModel: UpdateJoinStreamViewModel 15 | @EnvironmentObject var streamStore: StreamStore 16 | 17 | var body: some View { 18 | VStack(alignment: .center, spacing: 10) { 19 | TextField("Operation Name", text: $viewModel.title).font(.headline) 20 | TextField("Description", text: $viewModel.description).font(.subheadline) 21 | Picker(selection: self.$viewModel.selectedOperator, label: Text("Select a Type").font(.footnote)) { 22 | ForEach(self.viewModel.operators, id: \.self) { operatorItem in 23 | Text(operatorItem).tag(operatorItem) 24 | } 25 | }.padding() 26 | Spacer() 27 | VStack(spacing: 10) { 28 | Button("Cancel") { 29 | self.presentationMode.wrappedValue.dismiss() 30 | }.foregroundColor(Color.white) 31 | .frame(maxWidth: .infinity, minHeight: 50) 32 | .background(Color.gray) 33 | Button("Save") { 34 | self.viewModel.save() 35 | self.streamStore.save(self.viewModel.operationModel) 36 | self.presentationMode.wrappedValue.dismiss() 37 | }.foregroundColor(Color.white) 38 | .frame(maxWidth: .infinity, minHeight: 50) 39 | .background(Color.blue) 40 | }.padding(.top, 30) 41 | }.multilineTextAlignment(.center).padding(.top, 15) 42 | } 43 | } 44 | 45 | //struct UpdateJoinStreamView_Previews: PreviewProvider { 46 | // static var previews: some View { 47 | // UpdateJoinStreamView() 48 | // } 49 | //} 50 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/UpdateJoinStreamView/UpdateJoinStreamViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateJoinStreamViewModel.swift 3 | // combine-playground 4 | // 5 | // Created by kevin.cheng on 11/20/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import CombineExtensions 12 | class UpdateJoinStreamViewModel: ObservableObject { 13 | let operators = ["zip", "combine_latest"] 14 | 15 | @Published var selectedOperator = "zip" 16 | 17 | @Published var title: String 18 | 19 | @Published var description: String 20 | 21 | @Published var sourceStreamModels: [StreamModel] 22 | 23 | @Published var parameterTitle: String = "" 24 | 25 | @Published var operationModel: JoinOperationStreamModel 26 | 27 | private var stagingOperationModel: JoinOperationStreamModel 28 | 29 | var disposables: CancellableSet = CancellableSet() 30 | 31 | convenience init(sourceStreamModels: [StreamModel]) { 32 | self.init(sourceStreamModels: sourceStreamModels, 33 | operationModel: JoinOperationStreamModel(id: UUID(), name: nil, description: nil, 34 | operatorItem: .zip)) 35 | } 36 | 37 | init(sourceStreamModels: [StreamModel], operationModel: JoinOperationStreamModel) { 38 | self.sourceStreamModels = sourceStreamModels 39 | self.operationModel = operationModel 40 | self.stagingOperationModel = operationModel 41 | self.title = operationModel.name ?? "" 42 | self.description = operationModel.description ?? "" 43 | $selectedOperator.map { 44 | self.operators[self.operators.firstIndex(of: $0) ?? 0] 45 | }.assign(to: \.parameterTitle, on: self) 46 | .store(in: &disposables) 47 | 48 | switch operationModel.operatorItem { 49 | case .zip: 50 | selectedOperator = "zip" 51 | case .combineLatest: 52 | selectedOperator = "combine_latest" 53 | } 54 | setupBindings() 55 | } 56 | 57 | func setupBindings() { 58 | Publishers.CombineLatest3($title, $description, $selectedOperator) 59 | .map { (title, description, selectedOpt) -> JoinOperationStreamModel in 60 | let opt: JoinOperator 61 | switch selectedOpt { 62 | case "zip": 63 | opt = .zip 64 | case "flatMap": 65 | opt = .combineLatest 66 | default: 67 | opt = .zip 68 | } 69 | return JoinOperationStreamModel(id: self.operationModel.id, name: title, 70 | description: description, operatorItem: opt) 71 | }.assign(to: \.stagingOperationModel, on: self) 72 | .store(in: &disposables) 73 | } 74 | 75 | func save() { 76 | operationModel = stagingOperationModel 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/UpdateOperationStreamView/UpdateOperationStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateOperationStreamView.swift 3 | // combine-playground 4 | // 5 | // Created by kevin.cheng on 11/20/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct UpdateOperationStreamView: View { 13 | @Environment(\.presentationMode) var presentationMode: Binding 14 | @ObservedObject var viewModel: UpdateOperationStreamViewModel 15 | @EnvironmentObject var streamStore: StreamStore 16 | var body: some View { 17 | VStack(alignment: .center, spacing: 10) { 18 | TextField("Operation Name", text: $viewModel.title).font(.headline) 19 | TextField("Description", text: $viewModel.description).font(.subheadline) 20 | Picker(selection: self.$viewModel.selectedOperator, label: Text("Select a Type").font(.footnote)) { 21 | ForEach(self.viewModel.operators, id: \.self) { operatorItem in 22 | Text(operatorItem).tag(operatorItem) 23 | } 24 | }.padding() 25 | TextField(self.viewModel.parameterTitle, text: $viewModel.parameter) 26 | .font(.body) 27 | Spacer() 28 | VStack(spacing: 10) { 29 | Button("Cancel") { 30 | self.presentationMode.wrappedValue.dismiss() 31 | }.foregroundColor(Color.white) 32 | .frame(maxWidth: .infinity, minHeight: 50) 33 | .background(Color.gray) 34 | Button("Save") { 35 | self.viewModel.save() 36 | self.streamStore.save(self.viewModel.operationStreamModel) 37 | self.presentationMode.wrappedValue.dismiss() 38 | }.foregroundColor(Color.white) 39 | .frame(maxWidth: .infinity, minHeight: 50) 40 | .background(Color.blue) 41 | }.padding(.top, 30) 42 | }.multilineTextAlignment(.center).padding(.top, 15) 43 | } 44 | } 45 | 46 | //struct UpdateOperationStreamView_Previews: PreviewProvider { 47 | // static var previews: some View { 48 | // UpdateOperationStreamView(viewModel: UpdateOperationStreamViewModel( 49 | // streamStore: StreamStore(), 50 | // operationStreamModel: .init(id: UUID(), name: "New operation", 51 | // description: "delay", operatorItem: .map(expression: "%d * 3", next: nil)))) 52 | // } 53 | //} 54 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/UpdateStreamView/UpdateStreamViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateStreamViewModel.swift 3 | // CombineDemo 4 | // 5 | // Created by kevin.cheng on 8/27/19. 6 | // Copyright © 2019 Kevin Cheng. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import Combine 12 | 13 | extension ClosedRange where Bound == Unicode.Scalar { 14 | static let asciiPrintable: ClosedRange = " "..."~" 15 | var range: ClosedRange { return lowerBound.value...upperBound.value } 16 | var scalars: [Unicode.Scalar] { return range.compactMap(Unicode.Scalar.init) } 17 | var characters: [Character] { return scalars.map(Character.init) } 18 | var string: String { return String(scalars) } 19 | } 20 | 21 | extension String { 22 | init(_ sequence: S) where S.Element == Unicode.Scalar { 23 | self.init(UnicodeScalarView(sequence)) 24 | } 25 | } 26 | 27 | class UpdateStreamViewModel: ObservableObject { 28 | 29 | @Published var streamNumberOptions: [CircularTextViewModel] 30 | 31 | @Published var streamLetterOptions: [CircularTextViewModel] 32 | 33 | @Published var streamName: String 34 | 35 | @Published var streamDescription: String 36 | 37 | var sequenceDescription: String { 38 | streamModel.sequenceDescription 39 | } 40 | 41 | @Published var values: [IdentifiableValue] 42 | 43 | @Published var streamModel: StreamModel 44 | 45 | private var disposables = Set() 46 | 47 | init(streamModel: StreamModel) { 48 | self.streamModel = streamModel 49 | self.streamNumberOptions = (1...8).map { 50 | return CircularTextViewModel(value: String($0)) 51 | } 52 | self.streamLetterOptions = ("A"..."H").characters.map { 53 | return CircularTextViewModel(value: String($0)) 54 | } 55 | self.streamName = streamModel.name ?? "" 56 | self.streamDescription = streamModel.sequenceDescription 57 | self.values = streamModel.stream.map { 58 | return IdentifiableValue(value: $0.value) 59 | } 60 | self.setupDataBinding() 61 | } 62 | 63 | func setupDataBinding() { 64 | Publishers.CombineLatest($values, $streamName).map { (values, streamName) -> StreamModel in 65 | var newStream = self.streamModel 66 | newStream.stream = values.map { 67 | StreamItem(value: $0.value, operators: [.delay(seconds: 1)]) 68 | } 69 | newStream.name = streamName 70 | return newStream 71 | }.assign(to: \.streamModel, on: self) 72 | .store(in: &disposables) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/UpdateUnifyingStreamView/UpdateUnifyingStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateUnifyingStreamView.swift 3 | // combine-playground 4 | // 5 | // Created by kevin.cheng on 12/16/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct UpdateUnifyingStreamView: View { 13 | 14 | @Environment(\.presentationMode) var presentationMode: Binding 15 | @ObservedObject var viewModel: UpdateUnifyingStreamViewModel 16 | @EnvironmentObject var streamStore: StreamStore 17 | 18 | var body: some View { 19 | VStack(alignment: .center, spacing: 10) { 20 | TextField("Operation Name", text: $viewModel.title).font(.headline) 21 | TextField("Description", text: $viewModel.description).font(.subheadline) 22 | Picker(selection: self.$viewModel.selectedOperator, label: Text("Select a Type").font(.footnote)) { 23 | ForEach(self.viewModel.operators, id: \.self) { operatorItem in 24 | Text(operatorItem).tag(operatorItem) 25 | } 26 | }.padding() 27 | // TextField(self.viewModel.parameterTitle, text: self.$viewModel.parameter) 28 | // .font(.body) 29 | Spacer() 30 | VStack(spacing: 10) { 31 | Button("Cancel") { 32 | self.presentationMode.wrappedValue.dismiss() 33 | }.foregroundColor(Color.white) 34 | .frame(maxWidth: .infinity, minHeight: 50) 35 | .background(Color.gray) 36 | Button("Save") { 37 | self.viewModel.save() 38 | self.streamStore.save(self.viewModel.unifyingStreamModel) 39 | self.presentationMode.wrappedValue.dismiss() 40 | }.foregroundColor(Color.white) 41 | .frame(maxWidth: .infinity, minHeight: 50) 42 | .background(Color.blue) 43 | }.padding(.top, 30) 44 | }.multilineTextAlignment(.center).padding(.top, 15) 45 | } 46 | } 47 | 48 | //struct UpdateUnifyingStreamView_Previews: PreviewProvider { 49 | // static var previews: some View { 50 | // UpdateUnifyingStreamView() 51 | // } 52 | //} 53 | -------------------------------------------------------------------------------- /combine-playground/combine-playground/UpdateUnifyingStreamView/UpdateUnifyingStreamViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateUnifyingStreamViewModel.swift 3 | // combine-playground 4 | // 5 | // Created by kevin.cheng on 12/16/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import CombineExtensions 11 | import Combine 12 | class UpdateUnifyingStreamViewModel: ObservableObject { 13 | 14 | let operators = ["merge", "flatMap", "append"] 15 | 16 | @Published var selectedOperator = "merge" 17 | 18 | @Published var title: String 19 | 20 | @Published var description: String 21 | 22 | @Published var sourceStreamModels: [StreamModel] 23 | 24 | @Published var parameterTitle: String = "" 25 | 26 | @Published var unifyingStreamModel: UnifyingOperationStreamModel 27 | 28 | private var stagingStreamModel: UnifyingOperationStreamModel 29 | 30 | var disposables: CancellableSet = CancellableSet() 31 | 32 | convenience init(sourceStreamModels: [StreamModel]) { 33 | self.init(sourceStreamModels: sourceStreamModels, 34 | unifyingStreamModel: 35 | UnifyingOperationStreamModel(id: UUID(), name: nil, 36 | description: nil, operatorItem: .merge)) 37 | } 38 | 39 | init(sourceStreamModels: [StreamModel], unifyingStreamModel: UnifyingOperationStreamModel) { 40 | self.sourceStreamModels = sourceStreamModels 41 | self.unifyingStreamModel = unifyingStreamModel 42 | self.stagingStreamModel = unifyingStreamModel 43 | self.title = unifyingStreamModel.name ?? "" 44 | self.description = unifyingStreamModel.description ?? "" 45 | $selectedOperator.map { 46 | self.operators[self.operators.firstIndex(of: $0) ?? 0] 47 | }.assign(to: \UpdateUnifyingStreamViewModel.parameterTitle, on: self) 48 | .store(in: &disposables) 49 | 50 | switch unifyingStreamModel.operatorItem { 51 | case .merge: 52 | selectedOperator = "merge" 53 | case .flatMap: 54 | selectedOperator = "flatMap" 55 | case .append: 56 | selectedOperator = "append" 57 | } 58 | setupBindings() 59 | } 60 | 61 | func setupBindings() { 62 | Publishers.CombineLatest3($title, $description, $selectedOperator) 63 | .map { (title, description, selectedOpt) -> UnifyingOperationStreamModel in 64 | let opt: UnifyOparator 65 | switch selectedOpt { 66 | case "merge": 67 | opt = .merge 68 | case "flatMap": 69 | opt = .flatMap 70 | case "append": 71 | opt = .append 72 | default: 73 | opt = .append 74 | } 75 | return UnifyingOperationStreamModel(id: self.unifyingStreamModel.id, name: title, 76 | description: description, operatorItem: opt) 77 | }.assign(to: \.stagingStreamModel, on: self) 78 | .store(in: &disposables) 79 | } 80 | 81 | func save() { 82 | unifyingStreamModel = stagingStreamModel 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial/CircularTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircularTextView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CircularTextView: View { 12 | 13 | @State var text: String 14 | 15 | var body: some View { 16 | Text(text) 17 | .font(.system(size: 14)) 18 | .bold() 19 | .foregroundColor(Color.white) 20 | .padding() 21 | .background(Color.green) 22 | .clipShape(Circle()) 23 | .shadow(radius: 1) 24 | } 25 | } 26 | 27 | struct CircularTextView_Previews: PreviewProvider { 28 | static var previews: some View { 29 | CircularTextView(text: "A") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial/CombineStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 10/5/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct CombineStreamView: View { 13 | 14 | @State var streamValues: [String] = [] 15 | 16 | @State var cancellable: AnyCancellable? 17 | 18 | var body: some View { 19 | VStack(spacing: 30) { 20 | Spacer() 21 | TunnelView(streamValues: $streamValues) 22 | HStack { 23 | Button("Subscribe") { 24 | self.cancellable = self.invervalValuePublisher() 25 | .map { value in 26 | var newValues = self.streamValues 27 | newValues.append(value) 28 | return newValues 29 | }.assign(to: \.streamValues, on: self) 30 | } 31 | if self.cancellable != nil { 32 | Button("Cancel") { 33 | self.cancellable = nil 34 | } 35 | } else { 36 | Button("Clear") { 37 | self.streamValues.removeAll() 38 | } 39 | } 40 | } 41 | Spacer() 42 | } 43 | } 44 | 45 | func invervalValuePublisher() -> AnyPublisher { 46 | let publishers = (1...5).map { String($0) } 47 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 48 | return publishers[1...].reduce(publishers[0]) { 49 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 50 | } 51 | } 52 | } 53 | 54 | struct CombineStreamView_Previews: PreviewProvider { 55 | static var previews: some View { 56 | CombineStreamView() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | var body: some View { 13 | CombineStreamView() 14 | } 15 | } 16 | 17 | struct ContentView_Previews: PreviewProvider { 18 | static var previews: some View { 19 | ContentView() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Create the SwiftUI view that provides the window contents. 23 | let contentView = ContentView() 24 | 25 | // Use a UIHostingController as window root view controller. 26 | if let windowScene = scene as? UIWindowScene { 27 | let window = UIWindow(windowScene: windowScene) 28 | window.rootViewController = UIHostingController(rootView: contentView) 29 | self.window = window 30 | window.makeKeyAndVisible() 31 | } 32 | } 33 | 34 | func sceneDidDisconnect(_ scene: UIScene) { 35 | // Called as the scene is being released by the system. 36 | // This occurs shortly after the scene enters the background, or when its session is discarded. 37 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 38 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 39 | } 40 | 41 | func sceneDidBecomeActive(_ scene: UIScene) { 42 | // Called when the scene has moved from an inactive state to an active state. 43 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 44 | } 45 | 46 | func sceneWillResignActive(_ scene: UIScene) { 47 | // Called when the scene will move from an active state to an inactive state. 48 | // This may occur due to temporary interruptions (ex. an incoming phone call). 49 | } 50 | 51 | func sceneWillEnterForeground(_ scene: UIScene) { 52 | // Called as the scene transitions from the background to the foreground. 53 | // Use this method to undo the changes made on entering the background. 54 | } 55 | 56 | func sceneDidEnterBackground(_ scene: UIScene) { 57 | // Called as the scene transitions from the foreground to the background. 58 | // Use this method to save data, release shared resources, and store enough scene-specific state information 59 | // to restore the scene back to its current state. 60 | } 61 | 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial/StreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/25/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | struct StreamView: View { 12 | 13 | @State var streamValues: [String] = [] 14 | 15 | @State private var nextValue = 0 16 | 17 | var body: some View { 18 | VStack(spacing: 30) { 19 | Spacer() 20 | TunnelView(streamValues: $streamValues) 21 | HStack { 22 | Button("Add") { 23 | self.nextValue += 1 24 | self.streamValues.append(String(self.nextValue)) 25 | } 26 | Button("Remove") { 27 | self.streamValues.remove(at: 0) 28 | } 29 | } 30 | Spacer() 31 | } 32 | } 33 | } 34 | 35 | struct StreamView_Previews: PreviewProvider { 36 | static var previews: some View { 37 | StreamView() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter1/CombineTutorial/TunnelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TunnelView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct TunnelView: View { 12 | 13 | @Binding var streamValues: [String] 14 | 15 | let verticalPadding: CGFloat = 5 16 | 17 | let tunnelColor: Color = Color(red: 242/255.0, green: 242/255.0, blue: 242/255.0) 18 | 19 | var body: some View { 20 | HStack(spacing: verticalPadding) { 21 | ForEach(streamValues.reversed(), id: \.self) { value in 22 | CircularTextView(text: value) 23 | } 24 | }.padding(.horizontal, 5) 25 | .frame(maxWidth: .infinity, minHeight: 50, alignment: .trailing) 26 | .padding([.top, .bottom], verticalPadding) 27 | .background(tunnelColor) 28 | } 29 | } 30 | 31 | struct TunnelView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | Section { 34 | TunnelView(streamValues: .constant(["1"])) 35 | TunnelView(streamValues: .constant(["1", "2"])) 36 | TunnelView(streamValues: .constant(["1", "2", "3"])) 37 | }.previewLayout(.sizeThatFits) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/CircularTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircularTextView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CircularTextView: View { 12 | 13 | @State var text: String 14 | 15 | var body: some View { 16 | Text(text) 17 | .font(.system(size: 14)) 18 | .bold() 19 | .foregroundColor(Color.white) 20 | .padding() 21 | .frame(minWidth: 50, minHeight: 50) 22 | .background(Color.green) 23 | .clipShape(Circle()) 24 | .shadow(radius: 1) 25 | } 26 | } 27 | 28 | struct CircularTextView_Previews: PreviewProvider { 29 | static var previews: some View { 30 | CircularTextView(text: "A") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/CombineMapStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 10/5/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct CombineMapStreamView: View { 13 | 14 | @State private var stream1Values: [String] = [] 15 | 16 | @State private var stream2Values: [String] = [] 17 | 18 | @State private var disposables = Set() 19 | 20 | var body: some View { 21 | VStack(spacing: 30) { 22 | Spacer() 23 | TunnelView(streamValues: $stream1Values) 24 | TunnelView(streamValues: $stream2Values) 25 | HStack { 26 | Button("Subscribe") { 27 | self.disposables.forEach { 28 | $0.cancel() 29 | } 30 | self.disposables.removeAll() 31 | let publisher = self.invervalValuePublisher() 32 | publisher.sink { 33 | self.stream1Values.append($0) 34 | }.store(in: &self.disposables) 35 | let mapPublisher = publisher.map { (Int($0) ?? 0) * 2 }.map { String($0) }.eraseToAnyPublisher() 36 | mapPublisher.sink { 37 | self.stream2Values.append($0) 38 | }.store(in: &self.disposables) 39 | } 40 | if self.disposables.count > 0 { 41 | Button("Cancel") { 42 | self.disposables.removeAll() 43 | } 44 | } else { 45 | Button("Clear") { 46 | self.stream1Values.removeAll() 47 | self.stream2Values.removeAll() 48 | } 49 | } 50 | } 51 | Spacer() 52 | } 53 | } 54 | 55 | func invervalValuePublisher() -> AnyPublisher { 56 | let publishers = (1...5).map { String($0) } 57 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 58 | return publishers[1...].reduce(publishers[0]) { 59 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 60 | } 61 | } 62 | } 63 | 64 | struct CombineMapStreamView_Previews: PreviewProvider { 65 | static var previews: some View { 66 | CombineMapStreamView() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/CombineScanStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineScanStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 10/22/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct CombineScanStreamView: View { 13 | @State private var stream1Values: [String] = [] 14 | 15 | @State private var stream2Values: [String] = [] 16 | 17 | @State private var disposables = Set() 18 | 19 | var body: some View { 20 | VStack(spacing: 30) { 21 | Spacer() 22 | TunnelView(streamValues: $stream1Values) 23 | TunnelView(streamValues: $stream2Values) 24 | HStack { 25 | Button("Subscribe") { 26 | self.disposables.forEach { 27 | $0.cancel() 28 | } 29 | let publisher = self.invervalValuePublisher() 30 | publisher.sink { 31 | self.stream1Values.append($0) 32 | }.store(in: &self.disposables) 33 | let scanPublisher = publisher.map { Int($0) ?? 0 }.scan(0) { $0 + $1 }.map { String($0) } 34 | scanPublisher.sink { 35 | self.stream2Values.append($0) 36 | }.store(in: &self.disposables) 37 | } 38 | if self.disposables.count > 0 { 39 | Button("Cancel") { 40 | self.disposables.removeAll() 41 | } 42 | } else { 43 | Button("Clear") { 44 | self.stream1Values.removeAll() 45 | self.stream2Values.removeAll() 46 | } 47 | } 48 | } 49 | Spacer() 50 | } 51 | } 52 | 53 | func invervalValuePublisher() -> AnyPublisher { 54 | let publishers = (1...5).map { String($0) } 55 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 56 | return publishers[1...].reduce(publishers[0]) { 57 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 58 | } 59 | } 60 | } 61 | 62 | struct CombineScanStreamView_Previews: PreviewProvider { 63 | static var previews: some View { 64 | CombineScanStreamView() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/CombineStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 10/5/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct CombineStreamView: View { 13 | 14 | @State var streamValues: [String] = [] 15 | 16 | @State var cancellable: AnyCancellable? 17 | 18 | var body: some View { 19 | VStack(spacing: 30) { 20 | Spacer() 21 | TunnelView(streamValues: $streamValues) 22 | HStack { 23 | Button("Subscribe") { 24 | self.cancellable?.cancel() 25 | self.cancellable = self.invervalValuePublisher() 26 | .sink(receiveCompletion: { _ in 27 | self.cancellable = nil 28 | }, receiveValue: { 29 | self.streamValues.append($0) 30 | }) 31 | } 32 | if self.cancellable != nil { 33 | Button("Cancel") { 34 | self.cancellable = nil 35 | } 36 | } else { 37 | Button("Clear") { 38 | self.streamValues.removeAll() 39 | } 40 | } 41 | } 42 | Spacer() 43 | } 44 | } 45 | 46 | func invervalValuePublisher() -> AnyPublisher { 47 | let publishers = (1...5).map { String($0) } 48 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 49 | return publishers[1...].reduce(publishers[0]) { 50 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 51 | } 52 | } 53 | } 54 | 55 | struct CombineStreamView_Previews: PreviewProvider { 56 | static var previews: some View { 57 | CombineStreamView() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct ContentView: View { 13 | var body: some View { 14 | NavigationView { 15 | List { 16 | NavigationLink(destination: GenericCombineStreamView(navigationBarTitle: "Map", description: ".map { $0 * 2 }", comparingPublisher: self.mapPublisher)) { 17 | Text("Map") 18 | } 19 | NavigationLink(destination: GenericCombineStreamView(navigationBarTitle: "Scan", description: ".scan(0) { $0 + $1 }", comparingPublisher: self.scanPublisher)) { 20 | Text("Scan") 21 | } 22 | NavigationLink(destination: GenericCombineStreamView(navigationBarTitle: "Filter", description: ".filter { $0 != 2 }", comparingPublisher: self.filterPublisher)) { 23 | Text("Filter") 24 | } 25 | NavigationLink(destination: GenericCombineStreamView(navigationBarTitle: "Drop", description: ".dropFirst(2)", comparingPublisher: self.dropPublisher)) { 26 | Text("Drop") 27 | } 28 | }.navigationBarTitle("Combine Operators") 29 | } 30 | } 31 | 32 | func mapPublisher(publisher: AnyPublisher) -> AnyPublisher { 33 | publisher.map { (Int($0) ?? 0) * 2 }.map { String($0) }.eraseToAnyPublisher() 34 | } 35 | 36 | func scanPublisher(publisher: AnyPublisher) -> AnyPublisher { 37 | publisher.map { Int($0) ?? 0 }.scan(0) { $0 + $1 }.map { String($0) }.eraseToAnyPublisher() 38 | } 39 | 40 | func filterPublisher(publisher: AnyPublisher) -> AnyPublisher { 41 | publisher.filter { $0 != "2" }.eraseToAnyPublisher() 42 | } 43 | 44 | func dropPublisher(publisher: AnyPublisher) -> AnyPublisher { 45 | publisher.dropFirst(2).eraseToAnyPublisher() 46 | } 47 | } 48 | 49 | struct ContentView_Previews: PreviewProvider { 50 | static var previews: some View { 51 | ContentView() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/GenericCombineStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericCombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 10/22/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct GenericCombineStreamView: View { 13 | 14 | @State private var stream1Values: [String] = [] 15 | 16 | @State private var stream2Values: [String] = [] 17 | 18 | @State private var disposables = Set() 19 | 20 | var navigationBarTitle: String? 21 | 22 | var description: String? 23 | 24 | var comparingPublisher: (AnyPublisher) -> (AnyPublisher) 25 | 26 | var body: some View { 27 | VStack(spacing: 30) { 28 | Spacer() 29 | 30 | Text(description ?? "") 31 | .font(.system(.headline, design: .monospaced)) 32 | .lineLimit(nil).padding() 33 | 34 | TunnelView(streamValues: $stream1Values) 35 | TunnelView(streamValues: $stream2Values) 36 | HStack { 37 | Button("Subscribe") { 38 | self.disposables.forEach { 39 | $0.cancel() 40 | } 41 | self.disposables.removeAll() 42 | let publisher = self.invervalValuePublisher() 43 | publisher.sink { 44 | self.stream1Values.append($0) 45 | }.store(in: &self.disposables) 46 | let comparingPublisher = self.comparingPublisher(publisher) 47 | comparingPublisher.sink { 48 | self.stream2Values.append($0) 49 | }.store(in: &self.disposables) 50 | } 51 | if self.disposables.count > 0 { 52 | Button("Cancel") { 53 | self.disposables.removeAll() 54 | } 55 | } else { 56 | Button("Clear") { 57 | self.stream1Values.removeAll() 58 | self.stream2Values.removeAll() 59 | } 60 | } 61 | } 62 | Spacer() 63 | }.navigationBarTitle(navigationBarTitle ?? "") 64 | } 65 | 66 | func invervalValuePublisher() -> AnyPublisher { 67 | let publishers = (1...5).map { String($0) } 68 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 69 | return publishers[1...].reduce(publishers[0]) { 70 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 71 | } 72 | } 73 | } 74 | 75 | struct GenericCombineStreamView_Previews: PreviewProvider { 76 | static var previews: some View { 77 | GenericCombineStreamView { 78 | $0.map { (Int($0) ?? 0) * 2 }.map { String($0) }.eraseToAnyPublisher() 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Create the SwiftUI view that provides the window contents. 23 | let contentView = ContentView() 24 | 25 | // Use a UIHostingController as window root view controller. 26 | if let windowScene = scene as? UIWindowScene { 27 | let window = UIWindow(windowScene: windowScene) 28 | window.rootViewController = UIHostingController(rootView: contentView) 29 | self.window = window 30 | window.makeKeyAndVisible() 31 | } 32 | } 33 | 34 | func sceneDidDisconnect(_ scene: UIScene) { 35 | // Called as the scene is being released by the system. 36 | // This occurs shortly after the scene enters the background, or when its session is discarded. 37 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 38 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 39 | } 40 | 41 | func sceneDidBecomeActive(_ scene: UIScene) { 42 | // Called when the scene has moved from an inactive state to an active state. 43 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 44 | } 45 | 46 | func sceneWillResignActive(_ scene: UIScene) { 47 | // Called when the scene will move from an active state to an inactive state. 48 | // This may occur due to temporary interruptions (ex. an incoming phone call). 49 | } 50 | 51 | func sceneWillEnterForeground(_ scene: UIScene) { 52 | // Called as the scene transitions from the background to the foreground. 53 | // Use this method to undo the changes made on entering the background. 54 | } 55 | 56 | func sceneDidEnterBackground(_ scene: UIScene) { 57 | // Called as the scene transitions from the foreground to the background. 58 | // Use this method to save data, release shared resources, and store enough scene-specific state information 59 | // to restore the scene back to its current state. 60 | } 61 | 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/StreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/25/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | struct StreamView: View { 12 | 13 | @State var streamValues: [String] = [] 14 | 15 | @State private var nextValue = 0 16 | 17 | var body: some View { 18 | VStack(spacing: 30) { 19 | Spacer() 20 | TunnelView(streamValues: $streamValues) 21 | HStack { 22 | Button("Add") { 23 | self.nextValue += 1 24 | self.streamValues.append(String(self.nextValue)) 25 | } 26 | Button("Remove") { 27 | self.streamValues.remove(at: 0) 28 | } 29 | } 30 | Spacer() 31 | } 32 | } 33 | } 34 | 35 | struct StreamView_Previews: PreviewProvider { 36 | static var previews: some View { 37 | StreamView() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter2/CombineTutorial/TunnelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TunnelView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct TunnelView: View { 12 | 13 | @Binding var streamValues: [String] 14 | 15 | let verticalPadding: CGFloat = 5 16 | 17 | let tunnelColor: Color = Color(red: 242/255.0, green: 242/255.0, blue: 242/255.0) 18 | 19 | var body: some View { 20 | HStack(spacing: verticalPadding) { 21 | ForEach(streamValues.reversed(), id: \.self) { value in 22 | CircularTextView(text: value) 23 | } 24 | }.padding(.horizontal, 5).padding(.vertical, 5) 25 | .frame(maxWidth: .infinity, minHeight: 60, alignment: .trailing) 26 | .padding([.top, .bottom], verticalPadding) 27 | .background(tunnelColor) 28 | } 29 | } 30 | 31 | struct TunnelView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | Section { 34 | TunnelView(streamValues: .constant(["1"])) 35 | TunnelView(streamValues: .constant(["1", "2"])) 36 | TunnelView(streamValues: .constant(["1", "2", "3"])) 37 | }.previewLayout(.sizeThatFits) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/CircularTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircularTextView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CircularTextView: View { 12 | 13 | @State var text: String 14 | 15 | var body: some View { 16 | Text(text) 17 | .font(.system(size: 14)) 18 | .bold() 19 | .foregroundColor(Color.white) 20 | .padding() 21 | .frame(minWidth: 50, minHeight: 50) 22 | .background(Color.green) 23 | .clipShape(Circle()) 24 | .shadow(radius: 1) 25 | } 26 | } 27 | 28 | struct CircularTextView_Previews: PreviewProvider { 29 | static var previews: some View { 30 | CircularTextView(text: "A") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/CombineMapStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 10/5/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct CombineMapStreamView: View { 13 | 14 | @State private var stream1Values: [String] = [] 15 | 16 | @State private var stream2Values: [String] = [] 17 | 18 | @State private var disposables = Set() 19 | 20 | var body: some View { 21 | VStack(spacing: 30) { 22 | Spacer() 23 | TunnelView(streamValues: $stream1Values) 24 | TunnelView(streamValues: $stream2Values) 25 | HStack { 26 | Button("Subscribe") { 27 | self.disposables.forEach { 28 | $0.cancel() 29 | } 30 | self.disposables.removeAll() 31 | let publisher = self.invervalValuePublisher() 32 | publisher.sink { 33 | self.stream1Values.append($0) 34 | }.store(in: &self.disposables) 35 | let mapPublisher = publisher.map { (Int($0) ?? 0) * 2 }.map { String($0) }.eraseToAnyPublisher() 36 | mapPublisher.sink { 37 | self.stream2Values.append($0) 38 | }.store(in: &self.disposables) 39 | } 40 | if self.disposables.count > 0 { 41 | Button("Cancel") { 42 | self.disposables.removeAll() 43 | } 44 | } else { 45 | Button("Clear") { 46 | self.stream1Values.removeAll() 47 | self.stream2Values.removeAll() 48 | } 49 | } 50 | } 51 | Spacer() 52 | } 53 | } 54 | 55 | func invervalValuePublisher() -> AnyPublisher { 56 | let publishers = (1...5).map { String($0) } 57 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 58 | return publishers[1...].reduce(publishers[0]) { 59 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 60 | } 61 | } 62 | } 63 | 64 | struct CombineMapStreamView_Previews: PreviewProvider { 65 | static var previews: some View { 66 | CombineMapStreamView() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/CombineScanStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineScanStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 10/22/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct CombineScanStreamView: View { 13 | @State private var stream1Values: [String] = [] 14 | 15 | @State private var stream2Values: [String] = [] 16 | 17 | @State private var disposables = Set() 18 | 19 | var body: some View { 20 | VStack(spacing: 30) { 21 | Spacer() 22 | TunnelView(streamValues: $stream1Values) 23 | TunnelView(streamValues: $stream2Values) 24 | HStack { 25 | Button("Subscribe") { 26 | self.disposables.forEach { 27 | $0.cancel() 28 | } 29 | let publisher = self.invervalValuePublisher() 30 | publisher.sink { 31 | self.stream1Values.append($0) 32 | }.store(in: &self.disposables) 33 | let scanPublisher = publisher.map { Int($0) ?? 0 }.scan(0) { $0 + $1 }.map { String($0) } 34 | scanPublisher.sink { 35 | self.stream2Values.append($0) 36 | }.store(in: &self.disposables) 37 | } 38 | if self.disposables.count > 0 { 39 | Button("Cancel") { 40 | self.disposables.removeAll() 41 | } 42 | } else { 43 | Button("Clear") { 44 | self.stream1Values.removeAll() 45 | self.stream2Values.removeAll() 46 | } 47 | } 48 | } 49 | Spacer() 50 | } 51 | } 52 | 53 | func invervalValuePublisher() -> AnyPublisher { 54 | let publishers = (1...5).map { String($0) } 55 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 56 | return publishers[1...].reduce(publishers[0]) { 57 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 58 | } 59 | } 60 | } 61 | 62 | struct CombineScanStreamView_Previews: PreviewProvider { 63 | static var previews: some View { 64 | CombineScanStreamView() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/CombineStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 10/5/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct CombineStreamView: View { 13 | 14 | @State var streamValues: [String] = [] 15 | 16 | @State var cancellable: AnyCancellable? 17 | 18 | var body: some View { 19 | VStack(spacing: 30) { 20 | Spacer() 21 | TunnelView(streamValues: $streamValues) 22 | HStack { 23 | Button("Subscribe") { 24 | self.cancellable?.cancel() 25 | self.cancellable = self.invervalValuePublisher() 26 | .sink(receiveCompletion: { _ in 27 | self.cancellable = nil 28 | }, receiveValue: { 29 | self.streamValues.append($0) 30 | }) 31 | } 32 | if self.cancellable != nil { 33 | Button("Cancel") { 34 | self.cancellable = nil 35 | } 36 | } else { 37 | Button("Clear") { 38 | self.streamValues.removeAll() 39 | } 40 | } 41 | } 42 | Spacer() 43 | } 44 | } 45 | 46 | func invervalValuePublisher() -> AnyPublisher { 47 | let publishers = (1...5).map { String($0) } 48 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 49 | return publishers[1...].reduce(publishers[0]) { 50 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 51 | } 52 | } 53 | } 54 | 55 | struct CombineStreamView_Previews: PreviewProvider { 56 | static var previews: some View { 57 | CombineStreamView() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct ContentView: View { 13 | var body: some View { 14 | NavigationView { 15 | List { 16 | NavigationLink(destination: DoublePublisherStreamView(navigationBarTitle: "Merge", description: "Publishers.Merge(publisher1, publisher2)", comparingPublisher: self.mergePublisher)) { 17 | Text("Merge") 18 | } 19 | NavigationLink(destination: DoublePublisherStreamView(navigationBarTitle: "Append", description: "publisher1.append(publisher2)", comparingPublisher: appendPublisher)) { 20 | Text("Append") 21 | } 22 | }.navigationBarTitle("Combine Operators") 23 | } 24 | } 25 | 26 | func mergePublisher(_ publisher1: AnyPublisher, 27 | _ publisher2: AnyPublisher) -> AnyPublisher{ 28 | Publishers.Merge(publisher1, publisher2).eraseToAnyPublisher() 29 | } 30 | 31 | func appendPublisher(_ publisher1: AnyPublisher, 32 | _ publisher2: AnyPublisher) -> AnyPublisher { 33 | publisher1.append(publisher2).eraseToAnyPublisher() 34 | } 35 | } 36 | 37 | struct ContentView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | ContentView() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/GenericCombineStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericCombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 10/22/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct GenericCombineStreamView: View { 13 | 14 | @State private var stream1Values: [String] = [] 15 | 16 | @State private var stream2Values: [String] = [] 17 | 18 | @State private var disposables = Set() 19 | 20 | var navigationBarTitle: String? 21 | 22 | var description: String? 23 | 24 | var comparingPublisher: (AnyPublisher) -> (AnyPublisher) 25 | 26 | var body: some View { 27 | VStack(spacing: 30) { 28 | Spacer() 29 | 30 | Text(description ?? "") 31 | .font(.system(.headline, design: .monospaced)) 32 | .lineLimit(nil).padding() 33 | 34 | TunnelView(streamValues: $stream1Values) 35 | TunnelView(streamValues: $stream2Values) 36 | HStack { 37 | Button("Subscribe") { 38 | self.disposables.forEach { 39 | $0.cancel() 40 | } 41 | self.disposables.removeAll() 42 | let publisher = self.invervalValuePublisher() 43 | publisher.sink { 44 | self.stream1Values.append($0) 45 | }.store(in: &self.disposables) 46 | let comparingPublisher = self.comparingPublisher(publisher) 47 | comparingPublisher.sink { 48 | self.stream2Values.append($0) 49 | }.store(in: &self.disposables) 50 | } 51 | if self.disposables.count > 0 { 52 | Button("Cancel") { 53 | self.disposables.removeAll() 54 | } 55 | } else { 56 | Button("Clear") { 57 | self.stream1Values.removeAll() 58 | self.stream2Values.removeAll() 59 | } 60 | } 61 | } 62 | Spacer() 63 | }.navigationBarTitle(navigationBarTitle ?? "") 64 | } 65 | 66 | func invervalValuePublisher() -> AnyPublisher { 67 | let publishers = (1...5).map { String($0) } 68 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 69 | return publishers[1...].reduce(publishers[0]) { 70 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 71 | } 72 | } 73 | } 74 | 75 | struct GenericCombineStreamView_Previews: PreviewProvider { 76 | static var previews: some View { 77 | GenericCombineStreamView { 78 | $0.map { (Int($0) ?? 0) * 2 }.map { String($0) }.eraseToAnyPublisher() 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Create the SwiftUI view that provides the window contents. 23 | let contentView = ContentView() 24 | 25 | // Use a UIHostingController as window root view controller. 26 | if let windowScene = scene as? UIWindowScene { 27 | let window = UIWindow(windowScene: windowScene) 28 | window.rootViewController = UIHostingController(rootView: contentView) 29 | self.window = window 30 | window.makeKeyAndVisible() 31 | } 32 | } 33 | 34 | func sceneDidDisconnect(_ scene: UIScene) { 35 | // Called as the scene is being released by the system. 36 | // This occurs shortly after the scene enters the background, or when its session is discarded. 37 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 38 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 39 | } 40 | 41 | func sceneDidBecomeActive(_ scene: UIScene) { 42 | // Called when the scene has moved from an inactive state to an active state. 43 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 44 | } 45 | 46 | func sceneWillResignActive(_ scene: UIScene) { 47 | // Called when the scene will move from an active state to an inactive state. 48 | // This may occur due to temporary interruptions (ex. an incoming phone call). 49 | } 50 | 51 | func sceneWillEnterForeground(_ scene: UIScene) { 52 | // Called as the scene transitions from the background to the foreground. 53 | // Use this method to undo the changes made on entering the background. 54 | } 55 | 56 | func sceneDidEnterBackground(_ scene: UIScene) { 57 | // Called as the scene transitions from the foreground to the background. 58 | // Use this method to save data, release shared resources, and store enough scene-specific state information 59 | // to restore the scene back to its current state. 60 | } 61 | 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/StreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/25/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | struct StreamView: View { 12 | 13 | @State var streamValues: [String] = [] 14 | 15 | @State private var nextValue = 0 16 | 17 | var body: some View { 18 | VStack(spacing: 30) { 19 | Spacer() 20 | TunnelView(streamValues: $streamValues) 21 | HStack { 22 | Button("Add") { 23 | self.nextValue += 1 24 | self.streamValues.append(String(self.nextValue)) 25 | } 26 | Button("Remove") { 27 | self.streamValues.remove(at: 0) 28 | } 29 | } 30 | Spacer() 31 | } 32 | } 33 | } 34 | 35 | struct StreamView_Previews: PreviewProvider { 36 | static var previews: some View { 37 | StreamView() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter3/CombineTutorial/TunnelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TunnelView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct TunnelView: View { 12 | 13 | @Binding var streamValues: [String] 14 | 15 | let verticalPadding: CGFloat = 5 16 | 17 | let tunnelColor: Color = Color(red: 242/255.0, green: 242/255.0, blue: 242/255.0) 18 | 19 | var body: some View { 20 | HStack(spacing: verticalPadding) { 21 | ForEach(streamValues.reversed(), id: \.self) { value in 22 | CircularTextView(text: value) 23 | } 24 | }.padding(.horizontal, 5).padding(.vertical, 5) 25 | .frame(maxWidth: .infinity, minHeight: 60, alignment: .trailing) 26 | .padding([.top, .bottom], verticalPadding) 27 | .background(tunnelColor) 28 | } 29 | } 30 | 31 | struct TunnelView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | Section { 34 | TunnelView(streamValues: .constant(["1"])) 35 | TunnelView(streamValues: .constant(["1", "2"])) 36 | TunnelView(streamValues: .constant(["1", "2", "3"])) 37 | }.previewLayout(.sizeThatFits) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/CircularTextArrayView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | struct CircularTextArrayView: View { 3 | 4 | var texts: [String] 5 | 6 | var body: some View { 7 | HStack(spacing: 0) { 8 | ForEach(texts, id: \.self) { value in 9 | CircularTextView(text: value) 10 | } 11 | } 12 | } 13 | } 14 | 15 | struct MultiCircularTextView_Previews: PreviewProvider { 16 | static var previews: some View { 17 | CircularTextArrayView(texts: ["1", "2"]).previewLayout(.sizeThatFits) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/CircularTextView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | struct CircularTextView: View { 3 | 4 | @State var text: String 5 | 6 | var body: some View { 7 | Text(text) 8 | .font(.system(size: 14)) 9 | .bold() 10 | .foregroundColor(Color.white) 11 | .padding() 12 | .frame(minWidth: 50, minHeight: 50) 13 | .background(Color.green) 14 | .clipShape(Circle()) 15 | .shadow(radius: 1) 16 | } 17 | } 18 | 19 | struct CircularTextView_Previews: PreviewProvider { 20 | static var previews: some View { 21 | Section { 22 | CircularTextView(text: "A") 23 | }.previewLayout(.sizeThatFits) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/CombineMapStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 10/5/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct CombineMapStreamView: View { 13 | 14 | @State private var stream1Values: [[String]] = [] 15 | 16 | @State private var stream2Values: [[String]] = [] 17 | 18 | @State private var disposables = Set() 19 | 20 | var body: some View { 21 | VStack(spacing: 30) { 22 | Spacer() 23 | TunnelView(streamValues: $stream1Values) 24 | TunnelView(streamValues: $stream2Values) 25 | HStack { 26 | Button("Subscribe") { 27 | self.disposables.forEach { 28 | $0.cancel() 29 | } 30 | self.disposables.removeAll() 31 | let publisher = self.invervalValuePublisher() 32 | publisher.sink { 33 | self.stream1Values.append([$0]) 34 | }.store(in: &self.disposables) 35 | let mapPublisher = publisher.map { (Int($0) ?? 0) * 2 }.map { String($0) }.eraseToAnyPublisher() 36 | mapPublisher.sink { 37 | self.stream2Values.append([$0]) 38 | }.store(in: &self.disposables) 39 | } 40 | if self.disposables.count > 0 { 41 | Button("Cancel") { 42 | self.disposables.removeAll() 43 | } 44 | } else { 45 | Button("Clear") { 46 | self.stream1Values.removeAll() 47 | self.stream2Values.removeAll() 48 | } 49 | } 50 | } 51 | Spacer() 52 | } 53 | } 54 | 55 | func invervalValuePublisher() -> AnyPublisher { 56 | let publishers = (1...5).map { String($0) } 57 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 58 | return publishers[1...].reduce(publishers[0]) { 59 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 60 | } 61 | } 62 | } 63 | 64 | struct CombineMapStreamView_Previews: PreviewProvider { 65 | static var previews: some View { 66 | CombineMapStreamView() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/CombineScanStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineScanStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 10/22/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct CombineScanStreamView: View { 13 | @State private var stream1Values: [String] = [] 14 | 15 | @State private var stream2Values: [String] = [] 16 | 17 | @State private var disposables = Set() 18 | 19 | var body: some View { 20 | VStack(spacing: 30) { 21 | Spacer() 22 | //TunnelView(streamValues: $stream1Values) 23 | //TunnelView(streamValues: $stream2Values) 24 | HStack { 25 | Button("Subscribe") { 26 | self.disposables.forEach { 27 | $0.cancel() 28 | } 29 | let publisher = self.invervalValuePublisher() 30 | publisher.sink { 31 | self.stream1Values.append($0) 32 | }.store(in: &self.disposables) 33 | let scanPublisher = publisher.map { Int($0) ?? 0 }.scan(0) { $0 + $1 }.map { String($0) } 34 | scanPublisher.sink { 35 | self.stream2Values.append($0) 36 | }.store(in: &self.disposables) 37 | } 38 | if self.disposables.count > 0 { 39 | Button("Cancel") { 40 | self.disposables.removeAll() 41 | } 42 | } else { 43 | Button("Clear") { 44 | self.stream1Values.removeAll() 45 | self.stream2Values.removeAll() 46 | } 47 | } 48 | } 49 | Spacer() 50 | } 51 | } 52 | 53 | func invervalValuePublisher() -> AnyPublisher { 54 | let publishers = (1...5).map { String($0) } 55 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 56 | return publishers[1...].reduce(publishers[0]) { 57 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 58 | } 59 | } 60 | } 61 | 62 | struct CombineScanStreamView_Previews: PreviewProvider { 63 | static var previews: some View { 64 | CombineScanStreamView() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/CombineStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 10/5/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct CombineStreamView: View { 13 | 14 | @State var streamValues: [[String]] = [] 15 | 16 | @State var cancellable: AnyCancellable? 17 | 18 | var body: some View { 19 | VStack(spacing: 30) { 20 | Spacer() 21 | TunnelView(streamValues: $streamValues) 22 | HStack { 23 | Button("Subscribe") { 24 | self.cancellable?.cancel() 25 | self.cancellable = self.invervalValuePublisher() 26 | .sink(receiveCompletion: { _ in 27 | self.cancellable = nil 28 | }, receiveValue: { 29 | self.streamValues.append([$0]) 30 | }) 31 | } 32 | if self.cancellable != nil { 33 | Button("Cancel") { 34 | self.cancellable = nil 35 | } 36 | } else { 37 | Button("Clear") { 38 | self.streamValues.removeAll() 39 | } 40 | } 41 | } 42 | Spacer() 43 | } 44 | } 45 | 46 | func invervalValuePublisher() -> AnyPublisher { 47 | let publishers = (1...5).map { String($0) } 48 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 49 | return publishers[1...].reduce(publishers[0]) { 50 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 51 | } 52 | } 53 | } 54 | 55 | struct CombineStreamView_Previews: PreviewProvider { 56 | static var previews: some View { 57 | CombineStreamView() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct ContentView: View { 13 | var body: some View { 14 | NavigationView { 15 | List { 16 | NavigationLink(destination: DoublePublisherStreamView(navigationBarTitle: "Zip", description: "Publishers.Zip(publisher1, publisher2)", comparingPublisher: self.zipPublisher)) { 17 | Text("Zip") 18 | } 19 | NavigationLink(destination: DoublePublisherStreamView(navigationBarTitle: "CombineLatest", description: "Publishers.CombineLatest(publisher1, publisher2)", comparingPublisher: self.combineLatestPublisher)) { 20 | Text("CombineLatest") 21 | } 22 | }.navigationBarTitle("Combine Operators") 23 | } 24 | } 25 | 26 | func zipPublisher(_ publisher1: AnyPublisher, 27 | _ publisher2: AnyPublisher) -> AnyPublisher<(String, String), Never>{ 28 | Publishers.Zip(publisher1, publisher2).eraseToAnyPublisher() 29 | } 30 | 31 | func combineLatestPublisher(_ publisher1: AnyPublisher, 32 | _ publisher2: AnyPublisher) -> AnyPublisher<(String, String), Never> { 33 | Publishers.CombineLatest(publisher1, publisher2).eraseToAnyPublisher() 34 | } 35 | 36 | } 37 | 38 | struct ContentView_Previews: PreviewProvider { 39 | static var previews: some View { 40 | ContentView() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/DoublePublisherStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineDoubleStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 10/27/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | struct DoublePublisherStreamView: View { 12 | @State private var stream1Values: [[String]] = [] 13 | 14 | @State private var stream2Values: [[String]] = [] 15 | 16 | @State private var streamResultValues: [[String]] = [] 17 | 18 | @State private var disposables = Set() 19 | 20 | var navigationBarTitle: String? 21 | 22 | var description: String? 23 | 24 | var comparingPublisher: (AnyPublisher, AnyPublisher) -> (AnyPublisher<(String, String), Never>) 25 | 26 | var body: some View { 27 | ScrollView { 28 | VStack(spacing: 30) { 29 | Spacer() 30 | Text(description ?? "") 31 | .font(.system(.headline, design: .monospaced)) 32 | .lineLimit(nil).padding() 33 | TunnelView(streamValues: $stream1Values) 34 | TunnelView(streamValues: $stream2Values) 35 | TunnelView(streamValues: $streamResultValues) 36 | HStack { 37 | Button("Subscribe") { 38 | self.disposables.forEach { 39 | $0.cancel() 40 | } 41 | self.disposables.removeAll() 42 | let publisher = self.invervalValuePublisher(array: ["1", "2", "3", "4"]) 43 | publisher.sink { 44 | self.stream1Values.append([$0]) 45 | }.store(in: &self.disposables) 46 | 47 | let publisher2 = self.invervalValuePublisher(array: ["A", "B", "C", "D"], interval: 1.5) 48 | publisher2.sink { 49 | self.stream2Values.append([$0]) 50 | }.store(in: &self.disposables) 51 | 52 | let comparingPublisher = self.comparingPublisher(publisher, publisher2) 53 | comparingPublisher.sink { 54 | self.streamResultValues.append([$0.0, $0.1]) 55 | }.store(in: &self.disposables) 56 | } 57 | if self.disposables.count > 0 { 58 | Button("Cancel") { 59 | self.disposables.removeAll() 60 | } 61 | } else { 62 | Button("Clear") { 63 | self.stream1Values.removeAll() 64 | self.stream2Values.removeAll() 65 | self.streamResultValues.removeAll() 66 | } 67 | } 68 | } 69 | Spacer() 70 | } 71 | }.navigationBarTitle(navigationBarTitle ?? "") 72 | } 73 | 74 | func invervalValuePublisher(array: [String], interval: Double = 1) -> AnyPublisher { 75 | let publishers = array 76 | .map { Just($0).delay(for: .seconds(interval), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 77 | return publishers[1...].reduce(publishers[0]) { 78 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 79 | } 80 | } 81 | } 82 | 83 | struct DoublePublisherStreamView_Previews: PreviewProvider { 84 | static var previews: some View { 85 | DoublePublisherStreamView { 86 | Publishers.Zip($0, $1).eraseToAnyPublisher() 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/GenericCombineStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericCombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 10/22/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct GenericCombineStreamView: View { 13 | 14 | @State private var stream1Values: [[String]] = [] 15 | 16 | @State private var stream2Values: [[String]] = [] 17 | 18 | @State private var disposables = Set() 19 | 20 | var navigationBarTitle: String? 21 | 22 | var description: String? 23 | 24 | var comparingPublisher: (AnyPublisher) -> (AnyPublisher) 25 | 26 | var body: some View { 27 | VStack(spacing: 30) { 28 | Spacer() 29 | 30 | Text(description ?? "") 31 | .font(.system(.headline, design: .monospaced)) 32 | .lineLimit(nil).padding() 33 | TunnelView(streamValues: $stream1Values) 34 | TunnelView(streamValues: $stream2Values) 35 | HStack { 36 | Button("Subscribe") { 37 | self.disposables.forEach { 38 | $0.cancel() 39 | } 40 | self.disposables.removeAll() 41 | let publisher = self.invervalValuePublisher() 42 | publisher.sink { 43 | self.stream1Values.append([$0]) 44 | }.store(in: &self.disposables) 45 | let comparingPublisher = self.comparingPublisher(publisher) 46 | comparingPublisher.sink { 47 | self.stream2Values.append([$0]) 48 | }.store(in: &self.disposables) 49 | } 50 | if self.disposables.count > 0 { 51 | Button("Cancel") { 52 | self.disposables.removeAll() 53 | } 54 | } else { 55 | Button("Clear") { 56 | self.stream1Values.removeAll() 57 | self.stream2Values.removeAll() 58 | } 59 | } 60 | } 61 | Spacer() 62 | }.navigationBarTitle(navigationBarTitle ?? "") 63 | } 64 | 65 | func invervalValuePublisher() -> AnyPublisher { 66 | let publishers = (1...5).map { String($0) } 67 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 68 | return publishers[1...].reduce(publishers[0]) { 69 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 70 | } 71 | } 72 | } 73 | 74 | struct GenericCombineStreamView_Previews: PreviewProvider { 75 | static var previews: some View { 76 | GenericCombineStreamView { 77 | $0.map { (Int($0) ?? 0) * 2 }.map { String($0) }.eraseToAnyPublisher() 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Create the SwiftUI view that provides the window contents. 23 | let contentView = ContentView() 24 | 25 | // Use a UIHostingController as window root view controller. 26 | if let windowScene = scene as? UIWindowScene { 27 | let window = UIWindow(windowScene: windowScene) 28 | window.rootViewController = UIHostingController(rootView: contentView) 29 | self.window = window 30 | window.makeKeyAndVisible() 31 | } 32 | } 33 | 34 | func sceneDidDisconnect(_ scene: UIScene) { 35 | // Called as the scene is being released by the system. 36 | // This occurs shortly after the scene enters the background, or when its session is discarded. 37 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 38 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 39 | } 40 | 41 | func sceneDidBecomeActive(_ scene: UIScene) { 42 | // Called when the scene has moved from an inactive state to an active state. 43 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 44 | } 45 | 46 | func sceneWillResignActive(_ scene: UIScene) { 47 | // Called when the scene will move from an active state to an inactive state. 48 | // This may occur due to temporary interruptions (ex. an incoming phone call). 49 | } 50 | 51 | func sceneWillEnterForeground(_ scene: UIScene) { 52 | // Called as the scene transitions from the background to the foreground. 53 | // Use this method to undo the changes made on entering the background. 54 | } 55 | 56 | func sceneDidEnterBackground(_ scene: UIScene) { 57 | // Called as the scene transitions from the foreground to the background. 58 | // Use this method to save data, release shared resources, and store enough scene-specific state information 59 | // to restore the scene back to its current state. 60 | } 61 | 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/StreamView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Combine 3 | struct StreamView: View { 4 | 5 | @State var streamValues: [[String]] = [] 6 | 7 | @State private var nextValue = 0 8 | 9 | var body: some View { 10 | VStack(spacing: 30) { 11 | Spacer() 12 | TunnelView(streamValues: $streamValues) 13 | HStack { 14 | Button("Add") { 15 | self.nextValue += 1 16 | self.streamValues.append([String(self.nextValue)]) 17 | } 18 | Button("Remove") { 19 | self.streamValues.remove(at: 0) 20 | } 21 | } 22 | Spacer() 23 | } 24 | } 25 | } 26 | 27 | struct StreamView_Previews: PreviewProvider { 28 | static var previews: some View { 29 | StreamView(streamValues: [["1", "A"], ["2", "B"]]).previewLayout(.sizeThatFits) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter4/CombineTutorial/TunnelView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | struct TunnelView: View { 3 | 4 | @Binding var streamValues: [[String]] 5 | 6 | let verticalPadding: CGFloat = 5 7 | 8 | let tunnelColor: Color = Color(red: 242/255.0, green: 242/255.0, blue: 242/255.0) 9 | 10 | var body: some View { 11 | HStack(spacing: verticalPadding) { 12 | ForEach(streamValues.reversed(), id: \.self) { texts in 13 | CircularTextArrayView(texts: texts) 14 | } 15 | }.padding(.horizontal, 5).padding(.vertical, 5) 16 | .frame(maxWidth: .infinity, minHeight: 60, alignment: .trailing) 17 | .padding([.top, .bottom], verticalPadding) 18 | .background(tunnelColor) 19 | } 20 | } 21 | 22 | struct TunnelView_Previews: PreviewProvider { 23 | static var previews: some View { 24 | Section { 25 | TunnelView(streamValues: .constant([["1"], ["2"], ["3"]])) 26 | TunnelView(streamValues: .constant([["A"], ["B"], ["C"]])) 27 | TunnelView(streamValues: .constant([["1", "A"], ["2", "B"], ["3", "C"]])) 28 | }.previewLayout(.sizeThatFits) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/ButtonModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonModifier.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 11/8/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ButtonModifier: ViewModifier { 12 | let backgroundColor: Color 13 | 14 | func body(content: Content) -> some View { 15 | content.font(.footnote) 16 | .padding(10) 17 | .foregroundColor(Color.white) 18 | .frame(minWidth: 80) 19 | .background(backgroundColor) 20 | .cornerRadius(12) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/CircularTextArrayView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | struct CircularTextArrayView: View { 3 | 4 | var texts: [String] 5 | 6 | var radius: CGFloat = 50 7 | 8 | var body: some View { 9 | HStack(spacing: 0) { 10 | ForEach(self.texts, id: \.self) { value in 11 | CircularTextView(text: value, radius: self.radius) 12 | } 13 | } 14 | } 15 | } 16 | 17 | struct MultiCircularTextView_Previews: PreviewProvider { 18 | static var previews: some View { 19 | CircularTextArrayView(texts: ["1", "2"]).previewLayout(.sizeThatFits) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/CircularTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircularTextView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CircularTextView: View { 12 | 13 | @State var text: String 14 | 15 | 16 | var radius: CGFloat = 50 17 | 18 | 19 | var body: some View { 20 | Text(text) 21 | .font(.system(size: 14)) 22 | .bold() 23 | .foregroundColor(Color.white) 24 | .padding() 25 | .frame(width: radius, height: radius) 26 | .background(Color.green) 27 | .clipShape(Circle()) 28 | .shadow(radius: 1) 29 | } 30 | } 31 | 32 | struct CircularTextView_Previews: PreviewProvider { 33 | static var previews: some View { 34 | Section { 35 | CircularTextView(text: "A") 36 | }.previewLayout(.sizeThatFits) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/CombineMapStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 10/5/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | import CombineExtensions 12 | struct CombineMapStreamView: View { 13 | 14 | @State private var stream1Values: [[String]] = [] 15 | 16 | @State private var stream2Values: [[String]] = [] 17 | 18 | @State private var disposables = Set() 19 | 20 | var body: some View { 21 | VStack(spacing: 30) { 22 | Spacer() 23 | TunnelView(streamValues: $stream1Values) 24 | TunnelView(streamValues: $stream2Values) 25 | HStack { 26 | Button("Subscribe") { 27 | self.disposables.cancelAll() 28 | let publisher = self.invervalValuePublisher() 29 | publisher.sink { 30 | self.stream1Values.append([$0]) 31 | }.store(in: &self.disposables) 32 | let mapPublisher = publisher.map { (Int($0) ?? 0) * 2 }.map { String($0) }.eraseToAnyPublisher() 33 | mapPublisher.sink { 34 | self.stream2Values.append([$0]) 35 | }.store(in: &self.disposables) 36 | } 37 | if self.disposables.count > 0 { 38 | Button("Cancel") { 39 | self.disposables.removeAll() 40 | } 41 | } else { 42 | Button("Clear") { 43 | self.stream1Values.removeAll() 44 | self.stream2Values.removeAll() 45 | } 46 | } 47 | } 48 | Spacer() 49 | } 50 | } 51 | 52 | func invervalValuePublisher() -> AnyPublisher { 53 | let publishers = (1...5).map { String($0) } 54 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 55 | return publishers[1...].reduce(publishers[0]) { 56 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 57 | } 58 | } 59 | } 60 | 61 | struct CombineMapStreamView_Previews: PreviewProvider { 62 | static var previews: some View { 63 | CombineMapStreamView() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/CombineScanStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineScanStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 10/22/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct CombineScanStreamView: View { 13 | @State private var stream1Values: [[String]] = [] 14 | 15 | @State private var stream2Values: [[String]] = [] 16 | 17 | @State private var disposables = Set() 18 | 19 | var body: some View { 20 | VStack(spacing: 30) { 21 | Spacer() 22 | TunnelView(streamValues: $stream1Values) 23 | TunnelView(streamValues: $stream2Values) 24 | HStack { 25 | Button("Subscribe") { 26 | self.disposables.cancelAll() 27 | let publisher = self.invervalValuePublisher() 28 | publisher.sink { 29 | self.stream1Values.append([$0]) 30 | }.store(in: &self.disposables) 31 | let scanPublisher = publisher.map { Int($0) ?? 0 }.scan(0) { $0 + $1 }.map { String($0) } 32 | scanPublisher.sink { 33 | self.stream2Values.append([$0]) 34 | }.store(in: &self.disposables) 35 | } 36 | if self.disposables.count > 0 { 37 | Button("Cancel") { 38 | self.disposables.removeAll() 39 | } 40 | } else { 41 | Button("Clear") { 42 | self.stream1Values.removeAll() 43 | self.stream2Values.removeAll() 44 | } 45 | } 46 | } 47 | Spacer() 48 | } 49 | } 50 | 51 | func invervalValuePublisher() -> AnyPublisher { 52 | let publishers = (1...5).map { String($0) } 53 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 54 | return publishers[1...].reduce(publishers[0]) { 55 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 56 | } 57 | } 58 | } 59 | 60 | struct CombineScanStreamView_Previews: PreviewProvider { 61 | static var previews: some View { 62 | CombineScanStreamView() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/CombineStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 10/5/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct CombineStreamView: View { 13 | 14 | @State var streamValues: [[String]] = [] 15 | 16 | @State var cancellable: AnyCancellable? 17 | 18 | var body: some View { 19 | VStack(spacing: 30) { 20 | Spacer() 21 | TunnelView(streamValues: $streamValues) 22 | HStack { 23 | Button("Subscribe") { 24 | self.cancellable?.cancel() 25 | self.cancellable = self.invervalValuePublisher() 26 | .sink(receiveCompletion: { _ in 27 | self.cancellable = nil 28 | }, receiveValue: { 29 | self.streamValues.append([$0]) 30 | }) 31 | }.modifier(ButtonModifier(backgroundColor: Color.blue)) 32 | if self.cancellable != nil { 33 | Button("Cancel") { 34 | self.cancellable = nil 35 | }.modifier(ButtonModifier(backgroundColor: Color.red)) 36 | } else { 37 | Button("Clear") { 38 | self.streamValues.removeAll() 39 | }.modifier(ButtonModifier(backgroundColor: Color.red)) } 40 | } 41 | Spacer() 42 | } 43 | } 44 | 45 | func invervalValuePublisher() -> AnyPublisher { 46 | let publishers = (1...5).map { String($0) } 47 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 48 | return publishers[1...].reduce(publishers[0]) { 49 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 50 | } 51 | } 52 | } 53 | 54 | struct CombineStreamView_Previews: PreviewProvider { 55 | static var previews: some View { 56 | CombineStreamView().previewLayout(.sizeThatFits) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct ContentView: View { 13 | var body: some View { 14 | NavigationView { 15 | List { 16 | NavigationLink(destination: DoublePublisherStreamView(navigationBarTitle: "Zip", description: "Publishers.Zip(publisher1, publisher2)", comparingPublisher: self.zipPublisher)) { 17 | Text("Zip") 18 | } 19 | NavigationLink(destination: DoublePublisherStreamView(navigationBarTitle: "CombineLatest", description: "Publishers.CombineLatest(publisher1, publisher2)", comparingPublisher: self.combineLatestPublisher)) { 20 | Text("CombineLatest") 21 | } 22 | }.navigationBarTitle("Combine Operators") 23 | } 24 | } 25 | 26 | func zipPublisher(_ publisher1: AnyPublisher, 27 | _ publisher2: AnyPublisher) -> AnyPublisher<(String, String), Never>{ 28 | Publishers.Zip(publisher1, publisher2).eraseToAnyPublisher() 29 | } 30 | 31 | func combineLatestPublisher(_ publisher1: AnyPublisher, 32 | _ publisher2: AnyPublisher) -> AnyPublisher<(String, String), Never> { 33 | Publishers.CombineLatest(publisher1, publisher2).eraseToAnyPublisher() 34 | } 35 | 36 | } 37 | 38 | struct ContentView_Previews: PreviewProvider { 39 | static var previews: some View { 40 | ContentView() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/DescriptiveTunnelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DescriptiveTunnelView.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 11/8/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct DescriptiveTunnelView: View { 12 | 13 | @Binding var streamValues: [[String]] 14 | 15 | var description: String 16 | 17 | var body: some View { 18 | VStack(spacing: 10) { 19 | Text(description).font(.system(.subheadline, design: .monospaced)) 20 | TunnelView(streamValues: $streamValues) 21 | } 22 | } 23 | } 24 | 25 | struct DescriptiveTunnelView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | DescriptiveTunnelView(streamValues: .constant([]), description: "") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/DoublePublisherStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineDoubleStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 10/27/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | struct DoublePublisherStreamView: View { 12 | @State private var stream1Values: [[String]] = [] 13 | 14 | @State private var stream2Values: [[String]] = [] 15 | 16 | @State private var streamResultValues: [[String]] = [] 17 | 18 | @State private var disposables = Set() 19 | 20 | var navigationBarTitle: String? 21 | 22 | var description: String? 23 | 24 | var comparingPublisher: (AnyPublisher, AnyPublisher) -> (AnyPublisher<(String, String), Never>) 25 | 26 | var body: some View { 27 | ScrollView { 28 | VStack(spacing: 30) { 29 | Spacer() 30 | DescriptiveTunnelView(streamValues: $stream1Values, description: "[1, 2, 3, 4]") 31 | DescriptiveTunnelView(streamValues: $stream2Values, description: "[A, B, C, D]") 32 | DescriptiveTunnelView(streamValues: $streamResultValues, description: description ?? "") 33 | HStack { 34 | Button("Subscribe") { 35 | self.disposables.cancelAll() 36 | let publisher = self.invervalValuePublisher(array: ["1", "2", "3", "4"]) 37 | publisher.sink { 38 | self.stream1Values.append([$0]) 39 | }.store(in: &self.disposables) 40 | 41 | let publisher2 = self.invervalValuePublisher(array: ["A", "B", "C", "D"], interval: 1.5) 42 | publisher2.sink { 43 | self.stream2Values.append([$0]) 44 | }.store(in: &self.disposables) 45 | 46 | let comparingPublisher = self.comparingPublisher(publisher, publisher2) 47 | comparingPublisher.sink { 48 | self.streamResultValues.append([$0.0, $0.1]) 49 | }.store(in: &self.disposables) 50 | }.modifier(ButtonModifier(backgroundColor: Color.blue)) 51 | if self.disposables.count > 0 { 52 | Button("Cancel") { 53 | self.disposables.removeAll() 54 | }.modifier(ButtonModifier(backgroundColor: Color.red)) 55 | } else { 56 | Button("Clear") { 57 | self.stream1Values.removeAll() 58 | self.stream2Values.removeAll() 59 | self.streamResultValues.removeAll() 60 | }.modifier(ButtonModifier(backgroundColor: Color.red)) 61 | } 62 | } 63 | Spacer() 64 | } 65 | }.navigationBarTitle(navigationBarTitle ?? "") 66 | } 67 | 68 | func invervalValuePublisher(array: [String], interval: Double = 1) -> AnyPublisher { 69 | let publishers = array 70 | .map { Just($0).delay(for: .seconds(interval), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 71 | return publishers[1...].reduce(publishers[0]) { 72 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 73 | } 74 | } 75 | } 76 | 77 | struct DoublePublisherStreamView_Previews: PreviewProvider { 78 | static var previews: some View { 79 | DoublePublisherStreamView { 80 | Publishers.Zip($0, $1).eraseToAnyPublisher() 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/GenericCombineStreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericCombineStreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 10/22/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | struct GenericCombineStreamView: View { 13 | 14 | @State private var stream1Values: [[String]] = [] 15 | 16 | @State private var stream2Values: [[String]] = [] 17 | 18 | @State private var disposables = Set() 19 | 20 | var navigationBarTitle: String? 21 | 22 | var description: String? 23 | 24 | var comparingPublisher: (AnyPublisher) -> (AnyPublisher) 25 | 26 | var body: some View { 27 | VStack(spacing: 30) { 28 | Spacer() 29 | 30 | Text(description ?? "") 31 | .font(.system(.headline, design: .monospaced)) 32 | .lineLimit(nil).padding() 33 | TunnelView(streamValues: $stream1Values) 34 | TunnelView(streamValues: $stream2Values) 35 | HStack { 36 | Button("Subscribe") { 37 | self.disposables.cancelAll() 38 | let publisher = self.invervalValuePublisher() 39 | publisher.sink { self.stream1Values.append([$0]) 40 | }.store(in: &self.disposables) 41 | let comparingPublisher = self.comparingPublisher(publisher) 42 | comparingPublisher.sink { 43 | self.stream2Values.append([$0]) 44 | }.store(in: &self.disposables) 45 | } 46 | if self.disposables.count > 0 { 47 | Button("Cancel") { 48 | self.disposables.removeAll() 49 | } 50 | } else { 51 | Button("Clear") { 52 | self.stream1Values.removeAll() 53 | self.stream2Values.removeAll() 54 | } 55 | } 56 | } 57 | Spacer() 58 | }.navigationBarTitle(navigationBarTitle ?? "") 59 | } 60 | 61 | func invervalValuePublisher() -> AnyPublisher { 62 | let publishers = (1...5).map { String($0) } 63 | .map { Just($0).delay(for: .seconds(1), scheduler: DispatchQueue.main).eraseToAnyPublisher() } 64 | return publishers[1...].reduce(publishers[0]) { 65 | Publishers.Concatenate(prefix: $0, suffix: $1).eraseToAnyPublisher() 66 | } 67 | } 68 | } 69 | 70 | struct GenericCombineStreamView_Previews: PreviewProvider { 71 | static var previews: some View { 72 | GenericCombineStreamView { 73 | $0.map { (Int($0) ?? 0) * 2 }.map { String($0) }.eraseToAnyPublisher() 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/MultiCircularTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiCircularTextView.swift 3 | // CombineTutorial 4 | // 5 | // Created by Kevin Cheng on 10/30/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | struct MultiCircularTextView: View { 11 | 12 | var texts: [String] 13 | 14 | var body: some View { 15 | HStack(spacing: 0) { 16 | ForEach(texts, id: \.self) { value in 17 | CircularTextView(text: value) 18 | } 19 | } 20 | } 21 | } 22 | 23 | struct MultiCircularTextView_Previews: PreviewProvider { 24 | static var previews: some View { 25 | MultiCircularTextView(texts: ["1", "2"]) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Create the SwiftUI view that provides the window contents. 23 | let contentView = ContentView() 24 | 25 | // Use a UIHostingController as window root view controller. 26 | if let windowScene = scene as? UIWindowScene { 27 | let window = UIWindow(windowScene: windowScene) 28 | window.rootViewController = UIHostingController(rootView: contentView) 29 | self.window = window 30 | window.makeKeyAndVisible() 31 | } 32 | } 33 | 34 | func sceneDidDisconnect(_ scene: UIScene) { 35 | // Called as the scene is being released by the system. 36 | // This occurs shortly after the scene enters the background, or when its session is discarded. 37 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 38 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 39 | } 40 | 41 | func sceneDidBecomeActive(_ scene: UIScene) { 42 | // Called when the scene has moved from an inactive state to an active state. 43 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 44 | } 45 | 46 | func sceneWillResignActive(_ scene: UIScene) { 47 | // Called when the scene will move from an active state to an inactive state. 48 | // This may occur due to temporary interruptions (ex. an incoming phone call). 49 | } 50 | 51 | func sceneWillEnterForeground(_ scene: UIScene) { 52 | // Called as the scene transitions from the background to the foreground. 53 | // Use this method to undo the changes made on entering the background. 54 | } 55 | 56 | func sceneDidEnterBackground(_ scene: UIScene) { 57 | // Called as the scene transitions from the foreground to the background. 58 | // Use this method to save data, release shared resources, and store enough scene-specific state information 59 | // to restore the scene back to its current state. 60 | } 61 | 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/StreamView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StreamView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/25/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | import SwiftUI 9 | import Combine 10 | struct StreamView: View { 11 | 12 | @State var streamValues: [[String]] = [] 13 | 14 | @State private var nextValue = 0 15 | 16 | var body: some View { 17 | VStack(spacing: 30) { 18 | Spacer() 19 | TunnelView(streamValues: $streamValues) 20 | HStack { 21 | Button("Add") { 22 | self.nextValue += 1 23 | self.streamValues.append([String(self.nextValue)]) 24 | }.modifier(ButtonModifier(backgroundColor: Color.blue)) 25 | Button("Remove") { 26 | self.streamValues.remove(at: 0) 27 | }.modifier(ButtonModifier(backgroundColor: Color.red)) 28 | } 29 | Spacer() 30 | } 31 | } 32 | } 33 | 34 | struct StreamView_Previews: PreviewProvider { 35 | static var previews: some View { 36 | StreamView(streamValues: [["1", "A"], ["2", "B"]]).previewLayout(.sizeThatFits) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tutorials/combine-tutorial/chapter5/CombineTutorial/TunnelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TunnelView.swift 3 | // CombineTutorial 4 | // 5 | // Created by kevin.cheng on 9/24/19. 6 | // Copyright © 2019 Kevin-Cheng. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | struct TunnelView: View { 11 | 12 | @Binding var streamValues: [[String]] 13 | 14 | let spacing: CGFloat = 5 15 | 16 | let tunnelColor: Color = Color(red: 242/255.0, green: 242/255.0, blue: 242/255.0) 17 | 18 | let radius: CGFloat = 50 19 | 20 | var body: some View { 21 | GeometryReader { reader in 22 | HStack(spacing: self.spacing) { 23 | ForEach(self.streamValues.reversed(), id: \.self) { texts in 24 | CircularTextArrayView(texts: texts) 25 | .transition(.asymmetric(insertion: .offset(x: -reader.size.width, y: 0), 26 | removal: .offset(x: reader.size.width, y: 0))) 27 | } 28 | } 29 | .frame(width: self.tunnelWidth(with: reader.size.width), alignment: .trailing) 30 | .offset(x: self.tunnelOffset(with: reader.size.width)) 31 | 32 | }.animation(.easeInOut(duration: 1)) 33 | .padding([.top, .bottom], self.spacing) 34 | .frame(height: 60) 35 | .background(self.tunnelColor) 36 | } 37 | 38 | func tunnelWidth(with containerWidth: CGFloat) -> CGFloat { 39 | max(containerWidth, (radius * 2 + spacing) * CGFloat(streamValues.count)) 40 | } 41 | 42 | func tunnelOffset(with containerWidth: CGFloat) -> CGFloat { 43 | (tunnelWidth(with: containerWidth) - containerWidth) / 2 44 | } 45 | } 46 | 47 | struct TunnelView_Previews: PreviewProvider { 48 | static var previews: some View { 49 | Section { 50 | TunnelView(streamValues: .constant([["1"], ["2"], ["3"]])) 51 | TunnelView(streamValues: .constant([["A"], ["B"], ["C"]])) 52 | TunnelView(streamValues: .constant([["1", "A"], ["2", "B"], ["3", "C"]])) 53 | }.previewLayout(.sizeThatFits) 54 | } 55 | } 56 | --------------------------------------------------------------------------------