├── .gitignore ├── LICENSE.md ├── Operators └── Operators.playground │ ├── Pages │ ├── Append.xcplaygroundpage │ │ └── Contents.swift │ ├── CollectByTime.xcplaygroundpage │ │ └── Contents.swift │ ├── CollectByTimeOrCount.xcplaygroundpage │ │ └── Contents.swift │ ├── CombineLatest.xcplaygroundpage │ │ └── Contents.swift │ ├── Debounce.xcplaygroundpage │ │ └── Contents.swift │ ├── Debugging.xcplaygroundpage │ │ └── Contents.swift │ ├── Delay.xcplaygroundpage │ │ └── Contents.swift │ ├── Drop.xcplaygroundpage │ │ └── Contents.swift │ ├── Filter.xcplaygroundpage │ │ └── Contents.swift │ ├── Find.xcplaygroundpage │ │ └── Contents.swift │ ├── Merge.xcplaygroundpage │ │ └── Contents.swift │ ├── MesureInterval.xcplaygroundpage │ │ └── Contents.swift │ ├── Multicast.xcplaygroundpage │ │ └── Contents.swift │ ├── Multicast2.xcplaygroundpage │ │ └── Contents.swift │ ├── Prefix.xcplaygroundpage │ │ └── Contents.swift │ ├── Prepend.xcplaygroundpage │ │ └── Contents.swift │ ├── Replace.xcplaygroundpage │ │ └── Contents.swift │ ├── RetryAndCatch.xcplaygroundpage │ │ └── Contents.swift │ ├── Sequence.xcplaygroundpage │ │ └── Contents.swift │ ├── Share.xcplaygroundpage │ │ └── Contents.swift │ ├── ShareNotWork.xcplaygroundpage │ │ └── Contents.swift │ ├── SwitchToLatest.xcplaygroundpage │ │ └── Contents.swift │ ├── Throttle.xcplaygroundpage │ │ └── Contents.swift │ ├── Timeout.xcplaygroundpage │ │ └── Contents.swift │ ├── TimeoutError.xcplaygroundpage │ │ └── Contents.swift │ ├── Transform.xcplaygroundpage │ │ └── Contents.swift │ ├── Zip.xcplaygroundpage │ │ └── Contents.swift │ └── flatMap2.xcplaygroundpage │ │ └── Contents.swift │ ├── Sources │ └── Helper.swift │ └── contents.xcplayground ├── Presentation ├── CoreLocationCombine │ ├── CoreLocationCombine.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── CoreLocationCombine │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── LocationMonitor.swift │ │ ├── Preview.swift │ │ ├── SceneDelegate.swift │ │ └── ViewController.swift ├── ParallaxCombine │ ├── ParallaxCombine.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── ParallaxCombine │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── sample.imageset │ │ │ ├── Contents.json │ │ │ └── ダウンロード.png │ │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ │ ├── Info.plist │ │ ├── SceneDelegate.swift │ │ └── ViewController.swift └── Presentation.playground │ ├── Pages │ ├── Assign.xcplaygroundpage │ │ └── Contents.swift │ ├── Assign2.xcplaygroundpage │ │ └── Contents.swift │ ├── Bindings_OnlyUIKit.xcplaygroundpage │ │ └── Contents.swift │ ├── Bindings_SwiftUI_Combine.xcplaygroundpage │ │ └── Contents.swift │ ├── Bindings_UIKit_Combine.xcplaygroundpage │ │ └── Contents.swift │ ├── EraseToAnyPublsiher.xcplaygroundpage │ │ └── Contents.swift │ ├── FlatMap2.xcplaygroundpage │ │ └── Contents.swift │ ├── KVO.xcplaygroundpage │ │ └── Contents.swift │ ├── NotificationCenter.xcplaygroundpage │ │ └── Contents.swift │ ├── Published.xcplaygroundpage │ │ └── Contents.swift │ ├── Published2.xcplaygroundpage │ │ └── Contents.swift │ ├── Sink.xcplaygroundpage │ │ └── Contents.swift │ ├── URLSession.xcplaygroundpage │ │ └── Contents.swift │ └── WrappedExistingImplementation.xcplaygroundpage │ │ └── Contents.swift │ └── contents.xcplayground ├── Publishers ├── CustomPublisher │ ├── CustomPublisher.md │ └── Publishers.playground │ │ ├── Pages │ │ ├── CustomPublisher.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── CustomPublisher2.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── Deferred.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── Empty.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── Fail.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── Future.xcplaygroundpage │ │ │ └── Contents.swift │ │ └── Just.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── Sources │ │ └── Helper.swift │ │ └── contents.xcplayground └── Publishers.playground │ ├── Pages │ ├── Deferred.xcplaygroundpage │ │ └── Contents.swift │ ├── Empty.xcplaygroundpage │ │ └── Contents.swift │ ├── Fail.xcplaygroundpage │ │ └── Contents.swift │ ├── Future.xcplaygroundpage │ │ └── Contents.swift │ ├── Just.xcplaygroundpage │ │ └── Contents.swift │ └── Record.xcplaygroundpage │ │ └── Contents.swift │ ├── Sources │ └── Helper.swift │ └── contents.xcplayground ├── README.md ├── Recommend.md ├── Sampleapp ├── CombineCollection │ ├── CombineCollection.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── CombineCollection.xcscheme │ ├── CombineCollection │ │ ├── API │ │ │ ├── BreedListLoader.swift │ │ │ ├── DogImageListLoader.swift │ │ │ ├── DogWebAPI.swift │ │ │ ├── ImageDataLoader.swift │ │ │ ├── ImageDataWebLoader.swift │ │ │ └── Shared │ │ │ │ ├── HTTPClient.swift │ │ │ │ └── URLSessionHTTPClient.swift │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── Info.plist │ │ ├── Models │ │ │ ├── Breed.swift │ │ │ └── DogImage.swift │ │ ├── SceneDelegate.swift │ │ ├── ViewController.swift │ │ └── Views │ │ │ ├── Base.lproj │ │ │ └── Main.storyboard │ │ │ ├── BreedImagesGrid │ │ │ ├── BreedImagesGridViewController.swift │ │ │ ├── BreedImagesGridViewModel.swift │ │ │ └── ImageCell.swift │ │ │ ├── BreedList │ │ │ ├── BreedListViewController.swift │ │ │ ├── BreedListViewModel.swift │ │ │ └── DisplayBreed.swift │ │ │ ├── ErrorView.swift │ │ │ └── LoadingView.swift │ └── CombineCollectionTests │ │ ├── BreedListViewModelTests.swift │ │ ├── Helpers.swift │ │ └── Info.plist ├── ComplexUserRegistration │ ├── ComplexUserRegistration.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── ComplexUserRegistration │ │ ├── Address │ │ └── AddressViewController.swift │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ │ ├── Completion │ │ └── CompletionViewController.swift │ │ ├── Info.plist │ │ ├── Models │ │ ├── AddressCandidate.swift │ │ ├── AppState.swift │ │ ├── RegistrationInformation.swift │ │ └── Step.swift │ │ ├── Password │ │ └── PasswordViewController.swift │ │ ├── SceneDelegate.swift │ │ ├── UIControl+Combine.swift │ │ └── UserName │ │ └── UserNameViewController.swift ├── ComplexUserRegistrationSwiftUI │ ├── ComplexUserRegistrationSwiftUI.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── ComplexUserRegistrationSwiftUI │ │ ├── Address │ │ └── AddressView.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Completion │ │ └── CompletionView.swift │ │ ├── ComplexUserRegistrationApp.swift │ │ ├── Info.plist │ │ ├── Models │ │ ├── AddressCandidate.swift │ │ ├── AppState.swift │ │ ├── RegistrationInformation.swift │ │ └── Step.swift │ │ ├── Password │ │ └── PasswordView.swift │ │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ └── UserName │ │ └── UserNameView.swift ├── SwiftUICombineCollection │ ├── SwiftUICombineCollection.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── SwiftUICombineCollection │ │ ├── API │ │ ├── DogWebAPI.swift │ │ ├── HTTPClient.swift │ │ ├── ImageDataWebLoader.swift │ │ └── URLSessionHTTPClient.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── CombineSchedulerHelper.swift │ │ ├── ContentView.swift │ │ ├── DIContainer.swift │ │ ├── Info.plist │ │ ├── Models │ │ ├── Breed.swift │ │ └── DogImage.swift │ │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ ├── SwiftUICombineCollectionApp.swift │ │ └── Views │ │ ├── BreedImagesGrid │ │ ├── BreedImageGridView.swift │ │ ├── BreedImageView.swift │ │ ├── BreedImageViewModel.swift │ │ ├── BreedImagesGridViewModel.swift │ │ └── ImageLoaderCache.swift │ │ └── BreedList │ │ ├── BreedListView.swift │ │ ├── BreedListViewModel.swift │ │ ├── BreedRow.swift │ │ └── DisplayBreed.swift ├── UserRegistration │ ├── UserRegistration.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── UserRegistration.xcscheme │ └── UserRegistration │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── SceneDelegate.swift │ │ └── ViewController.swift ├── UserRegistrationCombine │ ├── UserRegistrationCombine.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── UserRegistrationCombine │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── SceneDelegate.swift │ │ ├── UIControl+Combine.swift │ │ └── ViewController.swift └── UserRegistrationCombineSwiftUI │ ├── UserRegistrationCombineSwiftUI.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── UserRegistrationCombineSwiftUI │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── UserRegistrationCombineSwiftUIApp.swift ├── Schedulers ├── CombineCollection_AnySchedular │ ├── AnyScheduler.md │ ├── CombineCollection │ │ ├── CombineCollection.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── CombineCollection.xcscheme │ │ ├── CombineCollection │ │ │ ├── API │ │ │ │ ├── BreedListLoader.swift │ │ │ │ ├── DogImageListLoader.swift │ │ │ │ ├── DogWebAPI.swift │ │ │ │ ├── HTTPClient.swift │ │ │ │ ├── ImageDataLoader.swift │ │ │ │ ├── ImageDataWebLoader.swift │ │ │ │ └── URLSessionHTTPClient.swift │ │ │ ├── AnyScheduler.swift │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj │ │ │ │ └── LaunchScreen.storyboard │ │ │ ├── ImmediateScheduler.swift │ │ │ ├── Info.plist │ │ │ ├── Models │ │ │ │ ├── Breed.swift │ │ │ │ └── DogImage.swift │ │ │ ├── SceneDelegate.swift │ │ │ ├── ViewController.swift │ │ │ └── Views │ │ │ │ ├── Base.lproj │ │ │ │ └── Main.storyboard │ │ │ │ ├── BreedImagesGrid │ │ │ │ ├── BreedImagesGridViewController.swift │ │ │ │ ├── BreedImagesGridViewModel.swift │ │ │ │ └── ImageCell.swift │ │ │ │ ├── BreedList │ │ │ │ ├── BreedListViewController.swift │ │ │ │ ├── BreedListViewModel.swift │ │ │ │ └── DisplayBreed.swift │ │ │ │ ├── ErrorView.swift │ │ │ │ └── LoadingView.swift │ │ └── CombineCollectionTests │ │ │ ├── BreedListViewModelTests.swift │ │ │ ├── Helpers.swift │ │ │ ├── Info.plist │ │ │ └── UsingRunOnMainQueueScheduler │ │ │ └── BreedListViewModelTests_pattern3.swift │ └── ImmediateScheduler.md ├── CombineCollection_RunOnMainScheduler │ ├── CombineCollection.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── CombineCollection.xcscheme │ ├── CombineCollection │ │ ├── API │ │ │ ├── BreedListLoader.swift │ │ │ ├── DogImageListLoader.swift │ │ │ ├── DogWebAPI.swift │ │ │ ├── HTTPClient.swift │ │ │ ├── ImageDataLoader.swift │ │ │ ├── ImageDataWebLoader.swift │ │ │ └── URLSessionHTTPClient.swift │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── Info.plist │ │ ├── Models │ │ │ ├── Breed.swift │ │ │ └── DogImage.swift │ │ ├── RunOnMainQueueScheduler.swift │ │ ├── SceneDelegate.swift │ │ ├── ViewController.swift │ │ └── Views │ │ │ ├── Base.lproj │ │ │ └── Main.storyboard │ │ │ ├── BreedImagesGrid │ │ │ ├── BreedImagesGridViewController.swift │ │ │ ├── BreedImagesGridViewModel.swift │ │ │ └── ImageCell.swift │ │ │ ├── BreedList │ │ │ ├── BreedListViewController.swift │ │ │ ├── BreedListViewModel.swift │ │ │ └── DisplayBreed.swift │ │ │ ├── ErrorView.swift │ │ │ └── LoadingView.swift │ ├── CombineCollectionTests │ │ ├── BreedListViewModelTests.swift │ │ ├── Helpers.swift │ │ ├── Info.plist │ │ └── UsingRunOnMainQueueScheduler │ │ │ ├── BreedListViewModelTests_pattern3.swift │ │ │ └── BreedListViewModelTests_usingPublisherSpy.swift │ └── RunOnMainThreadScheduler.md ├── Scheduler ├── Scheduler.md ├── SchedulerTest │ ├── SchedulerTest.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── SchedulerTest.xcscheme │ ├── SchedulerTest │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── Info.plist │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ └── SchedulerTestApp.swift │ └── SchedulerTestTests │ │ ├── AnyScheduler.swift │ │ ├── Helpers.swift │ │ ├── ImmediateScheduler.swift │ │ ├── Info.plist │ │ ├── RunOnMainQueueScheduler.swift │ │ ├── SchedulerTests.swift │ │ ├── SchedulerTests_AnyScheduler.swift │ │ ├── SchedulerTests_ImmediateScheduler.swift │ │ └── SchedulerTests_RunOnMainQueue.swift └── Schedulers.playground │ ├── Pages │ ├── receiveOn.xcplaygroundpage │ │ └── Contents.swift │ ├── subscribeOn.xcplaygroundpage │ │ └── Contents.swift │ └── subscribeOnNoSwitch.xcplaygroundpage │ │ └── Contents.swift │ └── contents.xcplayground └── Timelane ├── 1.png ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── 9.png └── Timelane.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | ## Keynote 38 | *.key 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 shiz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/CollectByTime.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 指定した時間間隔でOutputをまとめて出力する 9 | run("collectByTime") { 10 | let startDate = Date() 11 | let publisher = PassthroughSubject() 12 | publisher.collect(.byTime(DispatchQueue.main, 1.0)) 13 | .sink(receiveValue: { value in 14 | print("receiveTime: \(Date().timeIntervalSince(startDate))") 15 | print("receiveValue: \(value)") 16 | }) 17 | .store(in: &cancellables) 18 | 19 | let start = DispatchTime.now() 20 | publisher.send(1) 21 | DispatchQueue.main.asyncAfter(deadline: start + 0.5) { 22 | publisher.send(2) 23 | } 24 | DispatchQueue.main.asyncAfter(deadline: start + 1.0) { 25 | publisher.send(3) 26 | } 27 | DispatchQueue.main.asyncAfter(deadline: start + 1.5) { 28 | publisher.send(4) 29 | } 30 | DispatchQueue.main.asyncAfter(deadline: start + 2.0) { 31 | publisher.send(5) 32 | } 33 | 34 | } 35 | 36 | //: [Next](@next) 37 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/CollectByTimeOrCount.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 指定した時間経過または指定した個数が出力された時点でOutputをまとめて出力する 9 | run("collectByTimeOrCount") { 10 | let startDate = Date() 11 | let publisher = PassthroughSubject() 12 | publisher.collect(.byTimeOrCount(DispatchQueue.main, 2.0, 2)) 13 | .sink(receiveValue: { value in 14 | print("receiveTime: \(Date().timeIntervalSince(startDate))") 15 | print("receiveValue: \(value)") 16 | }) 17 | .store(in: &cancellables) 18 | 19 | let start = DispatchTime.now() 20 | publisher.send(1) 21 | DispatchQueue.main.asyncAfter(deadline: start + 0.5) { 22 | publisher.send(2) 23 | } 24 | DispatchQueue.main.asyncAfter(deadline: start + 1.0) { 25 | publisher.send(3) 26 | } 27 | DispatchQueue.main.asyncAfter(deadline: start + 1.5) { 28 | publisher.send(4) 29 | } 30 | DispatchQueue.main.asyncAfter(deadline: start + 2.0) { 31 | publisher.send(5) 32 | } 33 | 34 | } 35 | 36 | //: [Next](@next) 37 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/CombineLatest.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 2つのPublisherを結合して1つのPublisherを生成し 9 | // それぞれの最新の値をTupleにしたものをOutputとして出力する 10 | // それぞれが値を出力して初めてOutputを始める 11 | // それぞれの出力のタイミングでその時の最新の値をOutput出力する(Zipと異なる) 12 | // 下記の場合(1, "one")(2, "one")(2, "two")(2, "two")(3, "two")と出力される 13 | run("combineLatest") { 14 | let publisher1 = PassthroughSubject() 15 | let publisher2 = PassthroughSubject() 16 | 17 | publisher1 18 | .combineLatest(publisher2) 19 | .sink(receiveCompletion: { finished in 20 | print("receiveCompletion: \(finished)") 21 | }, receiveValue: { value in 22 | print("receiveValue: \(value)") 23 | }) 24 | .store(in: &cancellables) 25 | 26 | publisher1.send(1) 27 | publisher2.send("one") 28 | publisher1.send(2) 29 | publisher2.send("two") 30 | publisher1.send(3) 31 | publisher1.send(completion: .finished) 32 | publisher2.send(completion: .finished) 33 | } 34 | //: [Next](@next) 35 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Debounce.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 最後のOutputが出力されてから指定の時間経過までに 9 | // 新しくOutputが出力されていなければその値を出力する 10 | // debounceの待機時間中にCompletionが出力されると最後のOutputは出力されません 11 | run("debounce") { 12 | let startDate = Date() 13 | let publisher = PassthroughSubject() 14 | publisher 15 | .print() 16 | .debounce(for: 1.0, scheduler: DispatchQueue.main) 17 | .sink(receiveValue: { value in 18 | print("Time: \(Date().timeIntervalSince(startDate))") 19 | print("Value: \(value)") 20 | }) 21 | .store(in: &cancellables) 22 | 23 | let start = DispatchTime.now() 24 | publisher.send("こ") 25 | DispatchQueue.main.asyncAfter(deadline: start + 0.5) { 26 | publisher.send("こん") 27 | } 28 | DispatchQueue.main.asyncAfter(deadline: start + 1.0) { 29 | publisher.send("こんに") 30 | } 31 | DispatchQueue.main.asyncAfter(deadline: start + 3.0) { 32 | publisher.send("こんにち") 33 | } 34 | DispatchQueue.main.asyncAfter(deadline: start + 3.5) { 35 | publisher.send("こんにちわ") 36 | } 37 | } 38 | 39 | //: [Next](@next) 40 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Delay.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 指定した時間後にOutputを出力する 9 | run("delay") { 10 | let start = Date() 11 | let publisher = PassthroughSubject() 12 | let delayPublisher = publisher.delay(for: 1.0, scheduler: DispatchQueue.main) 13 | 14 | publisher 15 | .sink(receiveValue: { _ in print("publisher: \(Date().timeIntervalSince(start))") }) 16 | .store(in: &cancellables) 17 | 18 | delayPublisher 19 | .sink(receiveValue: { _ in print("delayPublisher: \(Date().timeIntervalSince(start))") }) 20 | .store(in: &cancellables) 21 | 22 | publisher.send(()) 23 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 24 | publisher.send(()) 25 | } 26 | } 27 | //: [Next](@next) 28 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Drop.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 指定した数のOutputの出力をスキップする 9 | run("dropFirst") { 10 | [1,2,3,4,5].publisher 11 | .dropFirst(2) 12 | .sink(receiveCompletion: { finished in 13 | print("receiveCompletion: \(finished)") 14 | }, receiveValue: { value in 15 | print("receiveValue: \(value)") 16 | }) 17 | .store(in: &cancellables) 18 | } 19 | 20 | // 指定した条件に該当しないOutputが見つかるまでOutputをスキップする 21 | run("drop(while:)") { 22 | [1,2,3,4,5].publisher 23 | .drop(while: { $0 < 3 }) 24 | .sink(receiveCompletion: { finished in 25 | print("receiveCompletion: \(finished)") 26 | }, receiveValue: { value in 27 | print("receiveValue: \(value)") 28 | }) 29 | .store(in: &cancellables) 30 | } 31 | 32 | // 指定したPublisherが出力するまでOutputをスキップする 33 | run("drop(untilOutputForm:)") { 34 | let start = PassthroughSubject() 35 | let output = PassthroughSubject() 36 | output 37 | .drop(untilOutputFrom: start) 38 | .sink(receiveCompletion: { finished in 39 | print("receiveCompletion: \(finished)") 40 | }, receiveValue: { value in 41 | print("receiveValue: \(value)") 42 | }) 43 | .store(in: &cancellables) 44 | 45 | output.send(1) 46 | output.send(2) 47 | output.send(3) 48 | 49 | start.send(()) 50 | 51 | output.send(4) 52 | } 53 | 54 | //: [Next](@next) 55 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Filter.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 条件にあったOutputのみ出力する 9 | run("filter") { 10 | [1,2,3,4,5].publisher 11 | .filter { $0.isMultiple(of: 2) } 12 | .sink(receiveCompletion: { finished in 13 | print("receiveCompletion: \(finished)") 14 | }, receiveValue: { value in 15 | print("receiveValue: \(value)") 16 | }) 17 | .store(in: &cancellables) 18 | } 19 | 20 | // 重複を除外する 21 | run("removeDuplicate") { 22 | [1,2,2,3,4,3,5].publisher 23 | .removeDuplicates() 24 | .sink(receiveCompletion: { finished in 25 | print("receiveCompletion: \(finished)") 26 | }, receiveValue: { value in 27 | print("receiveValue: \(value)") 28 | }) 29 | .store(in: &cancellables) 30 | } 31 | 32 | // nilを除外する 33 | run("compactMap") { 34 | ["1","one","2","two","3","three"].publisher 35 | .compactMap { Int($0) } 36 | .sink(receiveCompletion: { finished in 37 | print("receiveCompletion: \(finished)") 38 | }, receiveValue: { value in 39 | print("receiveValue: \(value)") 40 | }) 41 | .store(in: &cancellables) 42 | } 43 | 44 | // Outputを無視してCompletionのみを出力する 45 | run("ignoreOutput") { 46 | [1,2,3,4,5,6].publisher 47 | .ignoreOutput() 48 | .sink(receiveCompletion: { finished in 49 | print("receiveCompletion: \(finished)") 50 | }, receiveValue: { value in 51 | print("receiveValue: \(value)") 52 | }) 53 | .store(in: &cancellables) 54 | } 55 | 56 | //: [Next](@next) 57 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Find.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 条件にあった最初のOutputのみ出力する 9 | // 該当するOutputが見つかるとcancelされる 10 | run("first") { 11 | [1,2,3,4,5].publisher 12 | .print() 13 | // .first(where: { $0.isMultiple(of: 2) }) 14 | .first { $0.isMultiple(of: 2) } 15 | .sink(receiveCompletion: { finished in 16 | print("receiveCompletion: \(finished)") 17 | }, receiveValue: { value in 18 | print("receiveValue: \(value)") 19 | }) 20 | .store(in: &cancellables) 21 | } 22 | 23 | // 条件にあった最後のOutputのみ出力する 24 | // 該当するOutputが見つかってもOutputはcancelされない 25 | run("last") { 26 | [1,2,3,4,5].publisher 27 | .print() 28 | // .last(where: { $0.isMultiple(of: 2) }) 29 | .last { $0.isMultiple(of: 2) } 30 | .sink(receiveCompletion: { finished in 31 | print("receiveCompletion: \(finished)") 32 | }, receiveValue: { value in 33 | print("receiveValue: \(value)") 34 | }) 35 | .store(in: &cancellables) 36 | } 37 | 38 | //: [Next](@next) 39 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Merge.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 2つのPublisherを結合して1つのPublisherとして出力する 9 | // 2つのPublisherの型は一致していなければならない 10 | run("merge") { 11 | let publisher1 = PassthroughSubject() 12 | let publisher2 = PassthroughSubject() 13 | 14 | publisher1 15 | .merge(with: publisher2) 16 | .sink(receiveCompletion: { finished in 17 | print("receiveCompletion: \(finished)") 18 | }, receiveValue: { value in 19 | print("receiveValue: \(value)") 20 | }) 21 | .store(in: &cancellables) 22 | 23 | publisher1.send(1) 24 | publisher2.send(2) 25 | publisher1.send(3) 26 | publisher2.send(4) 27 | publisher1.send(completion: .finished) 28 | publisher2.send(completion: .finished) 29 | } 30 | 31 | //: [Next](@next) 32 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/MesureInterval.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 最後のOutputが出力されてから指定の時間経過後に 9 | // 待機中に出力された(※最初または最後の値)を出力する(※latestの設定によるはずですが今はどちらの値を指定しても同じ値が出力されるので調査中です) 10 | // 最後に出力された値から新しくOutputが出力されていなければ何も出力しない 11 | // throttleの待機時間中にCompletionが出力されると最後のOutputは出力されません 12 | run("measureInterval") { 13 | let publisher = PassthroughSubject() 14 | publisher 15 | .measureInterval(using: DispatchQueue.main) 16 | .sink(receiveValue: { 17 | print("Measure emitted: \(Double($0.magnitude) / 1_000_000_000.0)") 18 | }) 19 | .store(in: &cancellables) 20 | 21 | let start = DispatchTime.now() 22 | DispatchQueue.main.asyncAfter(deadline: start + 0.5) { 23 | publisher.send() 24 | } 25 | DispatchQueue.main.asyncAfter(deadline: start + 1.0) { 26 | publisher.send() 27 | } 28 | DispatchQueue.main.asyncAfter(deadline: start + 3.0) { 29 | publisher.send() 30 | } 31 | DispatchQueue.main.asyncAfter(deadline: start + 3.5) { 32 | publisher.send() 33 | } 34 | } 35 | 36 | //: [Next](@next) 37 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Multicast.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | // MARK: - Multicast 8 | 9 | private var cancellables = Set() 10 | 11 | // Shareと同様に新しいPublisherを毎回生成するのではなく 12 | // 同じPublisherからの出力を複数のSubscriberで共有できるようにする 13 | // Subscriptionは一度しか行われない 14 | // さらにconnectを呼ぶまで値の出力をしないので出力の開始タイミングをコントロールできる 15 | // Publisherの値をSubscriberに伝えるSubjectが必要 16 | run("Multicast") { 17 | let subject = PassthroughSubject() 18 | let pub = (1...3).publisher 19 | .delay(for: 1, scheduler: DispatchQueue.main) 20 | .map( { _ in return Int.random(in: 0...100) } ) 21 | .print() 22 | .share() 23 | .multicast(subject: subject) 24 | 25 | pub.sink { print ("Stream 1 received: \($0)") } 26 | .store(in: &cancellables) 27 | 28 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 29 | pub.sink { print ("Stream 2 received: \($0)") } 30 | .store(in: &cancellables) 31 | } 32 | 33 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 34 | pub.connect() 35 | .store(in: &cancellables) 36 | } 37 | } 38 | 39 | PlaygroundPage.current.needsIndefiniteExecution = true 40 | 41 | //: [Next](@next) 42 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Multicast2.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | // MARK: - Multicast 8 | 9 | private var cancellables = Set() 10 | 11 | // Shareと同様に新しいPublisherを毎回生成するのではなく 12 | // 同じPublisherからの出力を複数のSubscriberで共有できるようにする 13 | // Subscriptionは一度しか行われない 14 | // さらにconnectを呼ぶまで値の出力をしないので出力の開始タイミングをコントロールできる 15 | // makeConnectableはFailureがNeverの時にのみ利用できる 16 | run("Multicast makeConnectable") { 17 | let subject = PassthroughSubject() 18 | let pub = (1...3).publisher 19 | .delay(for: 1, scheduler: DispatchQueue.main) 20 | .map( { _ in return Int.random(in: 0...100) } ) 21 | .print() 22 | .share() 23 | .makeConnectable() 24 | 25 | pub.sink { print ("Stream 1 received: \($0)") } 26 | .store(in: &cancellables) 27 | 28 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 29 | pub.sink { print ("Stream 2 received: \($0)") } 30 | .store(in: &cancellables) 31 | } 32 | 33 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 34 | pub.connect() 35 | .store(in: &cancellables) 36 | } 37 | } 38 | 39 | PlaygroundPage.current.needsIndefiniteExecution = true 40 | 41 | //: [Next](@next) 42 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Prefix.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 指定した数のOutputを出力する 9 | run("prefix") { 10 | [1,2,3,4,5].publisher 11 | .prefix(2) 12 | .sink(receiveCompletion: { finished in 13 | print("receiveCompletion: \(finished)") 14 | }, receiveValue: { value in 15 | print("receiveValue: \(value)") 16 | }) 17 | .store(in: &cancellables) 18 | } 19 | 20 | // 指定した条件に該当しないOutputが出力されるまでOutputを出力する 21 | run("prefix(while:)") { 22 | [1,2,3,4,5].publisher 23 | .prefix(while: { $0 < 2 }) 24 | .sink(receiveCompletion: { finished in 25 | print("receiveCompletion: \(finished)") 26 | }, receiveValue: { value in 27 | print("receiveValue: \(value)") 28 | }) 29 | .store(in: &cancellables) 30 | } 31 | 32 | // 指定したPublisherが出力するまでOutputを出力する 33 | run("prefix(untilOutputForm:)") { 34 | let start = PassthroughSubject() 35 | let output = PassthroughSubject() 36 | output 37 | .prefix(untilOutputFrom: start) 38 | .sink(receiveCompletion: { finished in 39 | print("receiveCompletion: \(finished)") 40 | }, receiveValue: { value in 41 | print("receiveValue: \(value)") 42 | }) 43 | .store(in: &cancellables) 44 | 45 | output.send(1) 46 | output.send(2) 47 | output.send(3) 48 | 49 | start.send(()) 50 | 51 | output.send(4) 52 | } 53 | 54 | //: [Next](@next) 55 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Replace.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | private var cancellables = Set() 7 | 8 | // nilの値を変換する 9 | run("replaceNil") { 10 | [1,nil,nil,4].publisher 11 | .replaceNil(with: 999) 12 | .sink(receiveCompletion: { finished in 13 | print("receiveCompletion: \(finished)") 14 | }, receiveValue: { value in 15 | print("receiveValue: \(value ?? 0)") 16 | }) 17 | .store(in: &cancellables) 18 | } 19 | 20 | // EmptyなPublisherを変換する 21 | run("replaceEmpty") { 22 | let empty = Empty() 23 | empty 24 | .replaceEmpty(with: "Empty") 25 | .sink(receiveCompletion: { finished in 26 | print("receiveCompletion: \(finished)") 27 | }, receiveValue: { value in 28 | print("receiveValue: \(value)") 29 | }) 30 | .store(in: &cancellables) 31 | } 32 | 33 | // 現在の値と出力された値を使って新しい値を生成する 34 | // 毎回の結果を出力する 35 | run("scan") { 36 | (1...10).publisher 37 | .scan(0, +) 38 | .sink(receiveValue: { print("receiveValue: \($0)") }) 39 | .store(in: &cancellables) 40 | } 41 | 42 | // 現在の値と出力された値を使って新しい値を生成する 43 | // 最終結果を出力する 44 | run("reduce") { 45 | (1...10).publisher 46 | .reduce(0, +) 47 | .sink(receiveValue: { print("receiveValue: \($0)") }) 48 | .store(in: &cancellables) 49 | } 50 | 51 | run("multiple") { 52 | let publisher = ["1", "1", "one", "2", "2", "two", "3", "three"].publisher 53 | publisher 54 | .compactMap { Int($0) } 55 | .removeDuplicates() 56 | .filter { $0 > 1 } 57 | .map { $0 * 2 } 58 | .sink(receiveCompletion: { finished in 59 | print("receiveCompletion: \(finished)") 60 | }, receiveValue: { value in 61 | print("receiveValue: \(value)") 62 | }) 63 | .store(in: &cancellables) 64 | } 65 | //: [Next](@next) 66 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/RetryAndCatch.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | PlaygroundPage.current.needsIndefiniteExecution = true 7 | 8 | var cancellables = Set() 9 | 10 | // 受け取ったOutputを変換して出力する 11 | run("retry and catch") { 12 | let url = URL(string: "https://hogehogehoge.com")! 13 | var tryCount = 0 14 | URLSession.shared.dataTaskPublisher(for: url) 15 | .handleEvents( 16 | receiveSubscription: { _ in 17 | tryCount += 1 18 | print("Try\(tryCount)") 19 | }, 20 | receiveCompletion: { 21 | guard case .failure(let error) = $0 else { return } 22 | print("Try\(tryCount) error: \(error)") 23 | } 24 | ) 25 | .retry(3) 26 | .map(\.data) 27 | .catch { error in 28 | return Just(Data()) 29 | } 30 | .sink(receiveCompletion: { finished in 31 | print("receiveCompletion: \(finished)") 32 | }, receiveValue: { value in 33 | print("receiveValue: \(value)") 34 | }) 35 | .store(in: &cancellables) 36 | } 37 | 38 | //: [Next](@next) 39 | 40 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Share.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | // MARK: - Share 8 | 9 | private var cancellables = Set() 10 | 11 | // 新しいPublisherを毎回生成するのではなく 12 | // 同じPublisherからの出力を複数のSubscriberで共有できるようにする 13 | // Subscriptionは一度しか行われていない 14 | // Tip: ``Publishers/Share`` is effectively a combination of the ``Publishers/Multicast`` and ``PassthroughSubject`` publishers, with an implicit ``ConnectablePublisher/autoconnect()``. 15 | run("Share") { 16 | 17 | let pub = (1...3).publisher 18 | .delay(for: 1, scheduler: DispatchQueue.main) 19 | .map( { _ in return Int.random(in: 0...100) } ) 20 | .print("Random") 21 | .share() 22 | 23 | pub 24 | .sink { print ("Stream 1 received: \($0)")} 25 | .store(in: &cancellables) 26 | pub 27 | .sink { print ("Stream 2 received: \($0)")} 28 | .store(in: &cancellables) 29 | } 30 | 31 | PlaygroundPage.current.needsIndefiniteExecution = true 32 | 33 | //: [Next](@next) 34 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/ShareNotWork.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | // MARK: - Share 8 | 9 | // https://qiita.com/shiz/items/f089c93bdebfaef2196fhttps://qiita.com/shiz/items/f089c93bdebfaef2196f 10 | 11 | private var cancellables = Set() 12 | 13 | // タイミングを遅らせると 14 | // PublisherはCompletionを出力しているため 15 | // Outputを取得できない 16 | run("Share not work") { 17 | let shared = URLSession.shared 18 | .dataTaskPublisher(for: URL(string: "https://www.google.com")!) 19 | .map(\.data) 20 | .print("shared") 21 | .share() 22 | shared 23 | .sink( receiveCompletion: { print("subscription1 receiveCompletion \($0)") }, 24 | receiveValue: { print("subscription1 receiveValue: '\($0)'") }) 25 | .store(in: &cancellables) 26 | 27 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 28 | shared 29 | .sink(receiveCompletion: { print("subscription2 receiveCompletion \($0)")}, 30 | receiveValue: { print("subscription2 receiveValue: '\($0)'") }) 31 | .store(in: &cancellables) 32 | } 33 | } 34 | 35 | PlaygroundPage.current.needsIndefiniteExecution = true 36 | 37 | //: [Next](@next) 38 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/SwitchToLatest.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | PlaygroundPage.current.needsIndefiniteExecution = true 7 | 8 | var cancellables = Set() 9 | 10 | // 新しいPublisherを受け取ると 11 | // 前のPublisherはキャンセルして 12 | // 最新のPublisherのOutputを出力する 13 | // キャッシュのせいかもしれませんが繰り返すと下記はキャンセルされなくなります 14 | run("switchToLatest") { 15 | final class API { 16 | func load() -> AnyPublisher { 17 | return URLSession.shared 18 | .dataTaskPublisher(for: URL(string: "https://dog.ceo/api/breeds/list/all")!) 19 | .map(\.data) 20 | .print("API call") 21 | .eraseToAnyPublisher() 22 | } 23 | } 24 | 25 | let api = API() 26 | let taps = PassthroughSubject() 27 | 28 | taps 29 | .map { api.load() } 30 | .switchToLatest() 31 | .sink(receiveCompletion: { finished in 32 | print("receiveCompletion: \(finished)") 33 | }, receiveValue: { value in 34 | print("receiveValue: \(value)") 35 | }) 36 | .store(in: &cancellables) 37 | 38 | taps.send() 39 | 40 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 41 | taps.send() 42 | } 43 | 44 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 45 | taps.send() 46 | } 47 | } 48 | 49 | //: [Next](@next) 50 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Throttle.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 最後のOutputが出力されてから指定の時間経過後に 9 | // 待機中に出力された(※最初または最後の値)を出力する(※latestの設定によるはずですが今はどちらの値を指定しても同じ値が出力されるので調査中です) 10 | // 最後に出力された値から新しくOutputが出力されていなければ何も出力しない 11 | // throttleの待機時間中にCompletionが出力されると最後のOutputは出力されません 12 | run("throttle") { 13 | let startDate = Date() 14 | let publisher = PassthroughSubject() 15 | publisher 16 | .print() 17 | .throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true) 18 | .sink(receiveValue: { value in 19 | print("Time: \(Date().timeIntervalSince(startDate))") 20 | print("Value: \(value)") 21 | }) 22 | .store(in: &cancellables) 23 | 24 | let start = DispatchTime.now() 25 | publisher.send("こ") 26 | DispatchQueue.main.asyncAfter(deadline: start + 0.5) { 27 | publisher.send("こん") 28 | } 29 | DispatchQueue.main.asyncAfter(deadline: start + 1.0) { 30 | publisher.send("こんに") 31 | } 32 | DispatchQueue.main.asyncAfter(deadline: start + 3.0) { 33 | publisher.send("こんにち") 34 | } 35 | DispatchQueue.main.asyncAfter(deadline: start + 3.5) { 36 | publisher.send("こんにちわ") 37 | } 38 | } 39 | 40 | //: [Next](@next) 41 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Timeout.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 最後のOutputが出力されてから指定の時間経過後にCompletionする 9 | run("timeout") { 10 | let publisher = PassthroughSubject() 11 | publisher 12 | .timeout(2.0, scheduler: DispatchQueue.main) 13 | .sink(receiveCompletion: { _ in print("Completion") }, 14 | receiveValue: { _ in print("Value") }) 15 | .store(in: &cancellables) 16 | 17 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 18 | publisher.send() 19 | } 20 | 21 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { 22 | publisher.send() 23 | } 24 | } 25 | //: [Next](@next) 26 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/TimeoutError.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 最後のOutputが出力されてから指定の時間経過後にCompletion(failure)する 9 | run("timeout error") { 10 | enum TimeoutError: Error { 11 | case timeout 12 | } 13 | 14 | let publisher = PassthroughSubject() 15 | publisher 16 | .timeout(2.0, scheduler: DispatchQueue.main, customError: { .timeout }) 17 | .sink(receiveCompletion: { print("Completion:\($0)") }, 18 | receiveValue: { _ in print("Value") }) 19 | .store(in: &cancellables) 20 | 21 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 22 | publisher.send() 23 | } 24 | 25 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { 26 | publisher.send() 27 | } 28 | } 29 | 30 | //: [Next](@next) 31 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Pages/Zip.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // 2つのPublisherを結合して1つのPublisherを生成し 9 | // それぞれの最新の値をTupleにしたものをOutputとして出力する 10 | // それぞれが値を出力して初めてOutputを始める 11 | // それぞれのPublisherの出力数に合わせてOutput出力する(CombineLatestと異なる) 12 | // 下記の場合(1, "one")(2, "two")(3, "three")は出力されるが(4, "three")は出力されない 13 | run("zip") { 14 | let publisher1 = PassthroughSubject() 15 | let publisher2 = PassthroughSubject() 16 | 17 | publisher1 18 | .zip(publisher2) 19 | .sink(receiveCompletion: { finished in 20 | print("receiveCompletion: \(finished)") 21 | }, receiveValue: { value in 22 | print("receiveValue: \(value)") 23 | }) 24 | .store(in: &cancellables) 25 | 26 | publisher1.send(1) 27 | publisher2.send("one") 28 | publisher1.send(2) 29 | publisher2.send("two") 30 | publisher2.send("three") 31 | publisher1.send(3) 32 | publisher1.send(4) 33 | publisher1.send(completion: .finished) 34 | publisher2.send(completion: .finished) 35 | } 36 | 37 | //: [Next](@next) 38 | -------------------------------------------------------------------------------- /Operators/Operators.playground/Sources/Helper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func run(_ name: String, action: () -> Void) { 4 | print("\n------Operator name:", name, "------\n") 5 | action() 6 | } 7 | 8 | extension Thread { 9 | public var number: Int { 10 | let desc = self.description 11 | let threadNumber = try! NSRegularExpression(pattern: "number = (\\d+)", options: .caseInsensitive) 12 | if let numberMatches = threadNumber.firstMatch(in: desc, range: NSMakeRange(0, desc.count)) { 13 | let s = NSString(string: desc).substring(with: numberMatches.range(at: 1)) 14 | return Int(s) ?? 0 15 | } 16 | return 0 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Operators/Operators.playground/contents.xcplayground: -------------------------------------------------------------------------------- 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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Presentation/CoreLocationCombine/CoreLocationCombine.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Presentation/CoreLocationCombine/CoreLocationCombine.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Presentation/CoreLocationCombine/CoreLocationCombine/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CoreLocationCombine 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | @main 10 | class AppDelegate: UIResponder, UIApplicationDelegate { 11 | 12 | 13 | 14 | func application(_ application: UIApplication, 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, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 22 | // Called when a new scene session is being created. 23 | // Use this method to select a configuration to create the new scene with. 24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 25 | } 26 | 27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 28 | // Called when the user discards a scene session. 29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 31 | } 32 | 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Presentation/CoreLocationCombine/CoreLocationCombine/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Presentation/CoreLocationCombine/CoreLocationCombine/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Presentation/CoreLocationCombine/CoreLocationCombine/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Presentation/CoreLocationCombine/CoreLocationCombine/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 | -------------------------------------------------------------------------------- /Presentation/CoreLocationCombine/CoreLocationCombine/Preview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Preview.swift 3 | // CoreLocationCombine 4 | // 5 | // 6 | 7 | import SwiftUI 8 | 9 | struct Preview: View { 10 | private let content: Content 11 | init(_ content: Content) { 12 | self.content = content 13 | } 14 | 15 | private let devices = [ 16 | "iPhone SE", 17 | "iPhone 11", 18 | "iPad Pro (11-inch) (2nd generation)", 19 | ] 20 | 21 | var body: some View { 22 | ForEach(devices, id: \.self) { name in 23 | Group { 24 | self.content 25 | .previewDevice(PreviewDevice(rawValue: name)) 26 | .previewDisplayName(name) 27 | .colorScheme(.light) 28 | self.content 29 | .previewDevice(PreviewDevice(rawValue: name)) 30 | .previewDisplayName(name) 31 | .colorScheme(.dark) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Presentation/CoreLocationCombine/CoreLocationCombine/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // CoreLocationCombine 4 | // 5 | // 6 | 7 | import UIKit 8 | import MapKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | private let locationMonitor = LocationMonitor(manager: CLLocationManager()) 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | guard let scene = scene as? UIWindowScene else { return } 18 | 19 | let window = UIWindow(windowScene: scene) 20 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 21 | let viewController = storyboard.instantiateViewController( 22 | identifier: String(describing: ViewController.self)) { [self] coder in 23 | ViewController(coder: coder, locationMonitor: locationMonitor) 24 | } 25 | let navigationController = UINavigationController(rootViewController: viewController) 26 | window.rootViewController = navigationController 27 | self.window = window 28 | window.makeKeyAndVisible() 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Presentation/ParallaxCombine/ParallaxCombine.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Presentation/ParallaxCombine/ParallaxCombine.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Presentation/ParallaxCombine/ParallaxCombine/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ParallaxCombine 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | @main 10 | class AppDelegate: UIResponder, UIApplicationDelegate { 11 | 12 | 13 | 14 | func application(_ application: UIApplication, 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, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 22 | // Called when a new scene session is being created. 23 | // Use this method to select a configuration to create the new scene with. 24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 25 | } 26 | 27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 28 | // Called when the user discards a scene session. 29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 31 | } 32 | 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Presentation/ParallaxCombine/ParallaxCombine/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Presentation/ParallaxCombine/ParallaxCombine/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Presentation/ParallaxCombine/ParallaxCombine/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Presentation/ParallaxCombine/ParallaxCombine/Assets.xcassets/sample.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ダウンロード.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Presentation/ParallaxCombine/ParallaxCombine/Assets.xcassets/sample.imageset/ダウンロード.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Presentation/ParallaxCombine/ParallaxCombine/Assets.xcassets/sample.imageset/ダウンロード.png -------------------------------------------------------------------------------- /Presentation/ParallaxCombine/ParallaxCombine/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 | -------------------------------------------------------------------------------- /Presentation/ParallaxCombine/ParallaxCombine/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // ParallaxCombine 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 10 | 11 | var window: UIWindow? 12 | 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | guard let scene = scene as? UIWindowScene else { return } 16 | 17 | let window = UIWindow(windowScene: scene) 18 | let viewController = ViewController() 19 | window.rootViewController = viewController 20 | self.window = window 21 | window.makeKeyAndVisible() 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/Assign.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | class ViewModel { 9 | var number: Int = 0 { 10 | didSet { 11 | print("receiveValue: \(number)") 12 | } 13 | } 14 | } 15 | let viewModel = ViewModel() 16 | let publisher = [1,2,3].publisher 17 | publisher 18 | .assign(to: \.number, on: viewModel) 19 | .store(in: &cancellables) 20 | 21 | //: [Next](@next) 22 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/Assign2.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | 5 | class Weather { 6 | @Published var temperature: Double = 20 7 | } 8 | let weather = Weather() 9 | _ = weather.$temperature 10 | .sink { 11 | print ("Temperature now: \($0)") 12 | } 13 | [1,2,3].publisher 14 | .map { $0 * 2 } 15 | .assign(to: &weather.$temperature) 16 | 17 | //: [Next](@next) 18 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/Bindings_SwiftUI_Combine.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import SwiftUI 4 | import Combine 5 | import PlaygroundSupport 6 | 7 | struct ContentView: View { 8 | @StateObject var viewModel = ViewModel() 9 | @State var width: CGFloat = 0 10 | var body: some View { 11 | VStack(spacing: 8) { 12 | Text(viewModel.id ?? "") 13 | .background(GeometryReader { proxy in 14 | Color.clear.preference( 15 | key: TextWidthPreferenceKey.self, 16 | value: proxy.size.width 17 | ) 18 | }) 19 | Button { 20 | viewModel.setNewID() 21 | } 22 | label: { 23 | Text("update") 24 | .foregroundColor(Color.white) 25 | .frame(width: width) 26 | .background(Color.blue) 27 | } 28 | } 29 | .onPreferenceChange(TextWidthPreferenceKey.self) { 30 | width = $0 31 | } 32 | } 33 | } 34 | 35 | final class ViewModel: ObservableObject { 36 | @Published var id: String? = UUID().uuidString 37 | func setNewID() { 38 | id = UUID().uuidString 39 | } 40 | } 41 | 42 | 43 | private struct TextWidthPreferenceKey: PreferenceKey { 44 | static let defaultValue: CGFloat = 0 45 | static func reduce(value: inout CGFloat, 46 | nextValue: () -> CGFloat) { 47 | value = nextValue() 48 | } 49 | } 50 | 51 | let nav = UINavigationController( 52 | rootViewController: UIHostingController(rootView: ContentView())) 53 | 54 | PlaygroundPage.current.liveView = nav 55 | 56 | //: [Next](@next) 57 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/Bindings_UIKit_Combine.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import UIKit 5 | import PlaygroundSupport 6 | 7 | final class ViewController: UIViewController { 8 | private lazy var container: UIStackView = { 9 | let stack = UIStackView() 10 | stack.axis = .vertical 11 | stack.spacing = 8 12 | return stack 13 | }() 14 | 15 | private let label = UILabel() 16 | 17 | private lazy var button: UIButton = { 18 | let button = UIButton( 19 | primaryAction: UIAction(title: "update") { button in 20 | self.viewModel.setNewID() 21 | }) 22 | button.tintColor = .white 23 | button.backgroundColor = .systemBlue 24 | return button 25 | }() 26 | 27 | var viewModel: ViewModel! 28 | var cancelables = Set() 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | setup() 33 | viewModel.$id 34 | .assign(to: \.text, on: label) 35 | .store(in: &cancelables) 36 | } 37 | 38 | private func setup() { 39 | container.addArrangedSubview(label) 40 | container.addArrangedSubview(button) 41 | view.addSubview(container) 42 | container.translatesAutoresizingMaskIntoConstraints = false 43 | NSLayoutConstraint.activate([ 44 | container.centerXAnchor.constraint(equalTo: view.centerXAnchor), 45 | container.centerYAnchor.constraint(equalTo: view.centerYAnchor) 46 | ]) 47 | } 48 | } 49 | 50 | final class ViewModel { 51 | @Published var id: String? = UUID().uuidString 52 | func setNewID() { 53 | id = UUID().uuidString 54 | } 55 | } 56 | 57 | let viewController = ViewController() 58 | let viewModel = ViewModel() 59 | viewController.viewModel = viewModel 60 | 61 | let nav = UINavigationController(rootViewController: viewController) 62 | 63 | PlaygroundPage.current.liveView = nav 64 | //: [Next](@next) 65 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/EraseToAnyPublsiher.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | class ViewModel { 7 | @Published var isIdValid: Bool = false 8 | @Published var isPasswordValid: Bool = false 9 | @Published var isPasswordConfirmValid: Bool = false 10 | } 11 | 12 | let viewModel = ViewModel() 13 | 14 | func isValid(viewModel: ViewModel) 15 | -> AnyPublisher { 16 | Publishers 17 | .CombineLatest3(viewModel.$isIdValid, 18 | viewModel.$isPasswordValid, 19 | viewModel.$isPasswordConfirmValid) 20 | .map { $0 && $1 && $2 } 21 | .eraseToAnyPublisher() 22 | } 23 | 24 | 25 | //: [Next](@next) 26 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/FlatMap2.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import PlaygroundSupport 4 | import Foundation 5 | import Combine 6 | 7 | let urls: [URL] = "... この文字順通りに出力されます" 8 | .map(String.init).compactMap { (parameter) in 9 | var components = URLComponents() 10 | components.scheme = "https" 11 | components.path = "postman-echo.com/get" 12 | components.queryItems = [URLQueryItem(name: parameter, value: nil)] 13 | return components.url 14 | } 15 | struct Postman: Decodable { 16 | var args: [String: String] 17 | } 18 | 19 | var stream = "" 20 | let s = urls.compactMap { value in 21 | URLSession.shared.dataTaskPublisher(for: value) 22 | .tryMap { data, response -> Data in 23 | return data 24 | } 25 | .decode(type: Postman.self, decoder: JSONDecoder()) 26 | .catch {_ in 27 | Just(Postman(args: [:])) 28 | } 29 | } 30 | .publisher 31 | .flatMap(maxPublishers: .max(1)){$0} 32 | .sink(receiveCompletion: { (c) in 33 | print(stream) 34 | }, receiveValue: { (postman) in 35 | print(postman.args.keys.joined(), terminator: "", to: &stream) 36 | }) 37 | 38 | extension Collection where Element: Publisher { 39 | func serialize() -> AnyPublisher? { 40 | guard let start = self.first else { return nil } 41 | return self.dropFirst().reduce(start.eraseToAnyPublisher()) { 42 | return $0.append($1).eraseToAnyPublisher() 43 | } 44 | } 45 | } 46 | 47 | PlaygroundPage.current.needsIndefiniteExecution = true 48 | 49 | //: [Next](@next) 50 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/KVO.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import UIKit 4 | import Combine 5 | 6 | extension UIScrollView { 7 | var contentOffsetPublisher: AnyPublisher { 8 | publisher(for: \.contentOffset).eraseToAnyPublisher() 9 | } 10 | } 11 | 12 | //: [Next](@next) 13 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/NotificationCenter.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import UIKit 4 | import Combine 5 | 6 | extension UITextField { 7 | var textPublisher: AnyPublisher { 8 | NotificationCenter.default 9 | .publisher(for: UITextField.textDidChangeNotification, object: self) 10 | .compactMap { $0.object as? UITextField } 11 | .map { $0.text ?? "" } 12 | .eraseToAnyPublisher() 13 | } 14 | } 15 | 16 | //: [Next](@next) 17 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/Published.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | 5 | class Weather { 6 | @Published var temperature: Double = 20 7 | } 8 | let weather = Weather() 9 | _ = weather.$temperature 10 | .sink { 11 | print ("Temperature now: \($0)") 12 | } 13 | weather.temperature = 25 14 | 15 | //: [Next](@next) 16 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/Published2.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | 5 | class Temperature { 6 | var value: Int 7 | init(value: Int) { 8 | self.value = value 9 | } 10 | } 11 | 12 | class Weather { 13 | @Published var temperature: Temperature = Temperature(value: 1) 14 | } 15 | let weather = Weather() 16 | _ = weather.$temperature 17 | .sink { 18 | print ("Temperature now: \($0.value)") 19 | } 20 | weather.temperature.value = 2 21 | 22 | print(weather.temperature.value) 23 | 24 | //: [Next](@next) 25 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/Sink.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | var cancellables = Set() 6 | let publisher = [1,2,3].publisher 7 | let subscription = publisher.sink( 8 | receiveCompletion: { finished in 9 | print("receiveCompletion: \(finished)") 10 | }, 11 | receiveValue: { value in 12 | print("receiveValue: \(value)") 13 | }) 14 | 15 | publisher.sink( 16 | receiveCompletion: { finished in 17 | print("receiveCompletion: \(finished)") 18 | }, 19 | receiveValue: { value in 20 | print("receiveValue: \(value)") 21 | }).store(in: &cancellables) 22 | 23 | //: [Next](@next) 24 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/URLSession.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | PlaygroundPage.current.needsIndefiniteExecution = true 7 | 8 | struct Breed: Equatable, Decodable { 9 | let name: String 10 | let subBreeds: [Breed] 11 | } 12 | 13 | struct BreedListAPIModel: Decodable { 14 | let message: [String: [String]] 15 | let status: String 16 | } 17 | 18 | func load() -> AnyPublisher<[Breed], Error> { 19 | func convert(from model: BreedListAPIModel) -> [Breed] { 20 | let messages = model.message 21 | return messages.map { message in 22 | Breed(name: message.key, 23 | subBreeds: message.value 24 | .map { Breed(name: $0, subBreeds: []) }) 25 | } 26 | } 27 | 28 | return URLSession.shared.dataTaskPublisher(for: URL(string: "https://dog.ceo/api/breeds/list/all")!) 29 | .map(\.data) 30 | .decode(type: BreedListAPIModel.self, decoder: JSONDecoder()) 31 | .map(convert(from:)) 32 | .eraseToAnyPublisher() 33 | } 34 | 35 | let subscription = load() 36 | .sink(receiveCompletion: { finished in 37 | print("receiveCompletion: \(finished)") 38 | }, receiveValue: { value in 39 | print("receiveValue: \(value)") 40 | }) 41 | 42 | 43 | //: [Next](@next) 44 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/Pages/WrappedExistingImplementation.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | func load(from url: URL, completion: @escaping (Result) -> Void) { 7 | completion(.success(Data())) 8 | } 9 | 10 | func loadPublisher(from url: URL) -> AnyPublisher { 11 | Deferred { 12 | Future { promise in 13 | load(from: url, completion: promise) 14 | } 15 | }.eraseToAnyPublisher() 16 | } 17 | 18 | //: [Next](@next) 19 | -------------------------------------------------------------------------------- /Presentation/Presentation.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Publishers/CustomPublisher/Publishers.playground/Pages/Deferred.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | var cancellables = Set() 8 | 9 | // 処理が完了した時にResultを引数に受け取るコールバック(promise)を呼び出す 10 | // DeferredはSubscribeされていないと内部の処理を実行しない 11 | run("Deferred without Subscribe") { 12 | _ = Deferred { 13 | Future { promise in 14 | sleep(1) 15 | print("executed") 16 | promise(.success(1)) 17 | } 18 | } 19 | } 20 | 21 | // 処理が完了した時にResultを引数に受け取るコールバック(promise)を呼び出す 22 | // DeferredはSubscribeされていないと内部の処理を実行しない 23 | run("Deferred with Subscribe") { 24 | Deferred { 25 | Future { promise in 26 | sleep(1) 27 | print("executed") 28 | promise(.success(2)) 29 | } 30 | } 31 | .sink(receiveCompletion: { finished in 32 | print("receivedCompletion: \(finished)") 33 | }, receiveValue: { value in 34 | print("receivedValue: \(value)") 35 | }).store(in: &cancellables) 36 | } 37 | 38 | PlaygroundPage.current.needsIndefiniteExecution = true 39 | 40 | //: [Next](@next) 41 | -------------------------------------------------------------------------------- /Publishers/CustomPublisher/Publishers.playground/Pages/Empty.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | var cancellables = Set() 8 | 9 | // Outputを出力しないですぐにComlpetion(Success)を出力する 10 | run("Empty") { 11 | Empty() 12 | .sink(receiveCompletion: { finished in 13 | print("receivedCompletion: \(finished)") 14 | }, receiveValue: { value in 15 | print("receivedValue: \(value)") 16 | }).store(in: &cancellables) 17 | } 18 | 19 | PlaygroundPage.current.needsIndefiniteExecution = true 20 | 21 | //: [Next](@next) 22 | -------------------------------------------------------------------------------- /Publishers/CustomPublisher/Publishers.playground/Pages/Fail.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | var cancellables = Set() 8 | 9 | // Outputを出力しないですぐにComlpetion(Failure)を出力する 10 | run("Fail") { 11 | struct SampleError: Error {} 12 | Fail(error: SampleError()) 13 | .sink(receiveCompletion: { finished in 14 | print("receivedCompletion: \(finished)") 15 | }, receiveValue: { value in 16 | print("receivedValue: \(value)") 17 | }).store(in: &cancellables) 18 | } 19 | 20 | PlaygroundPage.current.needsIndefiniteExecution = true 21 | 22 | //: [Next](@next) 23 | -------------------------------------------------------------------------------- /Publishers/CustomPublisher/Publishers.playground/Pages/Future.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | var cancellables = Set() 8 | 9 | // 処理が完了した時にResultを引数に受け取るコールバック(promise)を呼び出す 10 | // Outputを流したらすぐにCompletionを出力する 11 | // FutureはSubscribeされていないくても内部の処理を実行する 12 | run("Future with Subscribe") { 13 | Future { promise in 14 | sleep(1) 15 | print("executed") 16 | promise(.success(1)) 17 | } 18 | .sink(receiveCompletion: { finished in 19 | print("receivedCompletion: \(finished)") 20 | }, receiveValue: { value in 21 | print("receivedValue: \(value)") 22 | }).store(in: &cancellables) 23 | } 24 | 25 | // 処理が完了した時にResultを引数に受け取るコールバック(promise)を呼び出す 26 | // FutureはSubscribeされていないくても内部の処理を実行する 27 | run("Future without Subscribe") { 28 | _ = Future { promise in 29 | sleep(1) 30 | print("executed") 31 | promise(.success(2)) 32 | } 33 | } 34 | 35 | PlaygroundPage.current.needsIndefiniteExecution = true 36 | 37 | //: [Next](@next) 38 | -------------------------------------------------------------------------------- /Publishers/CustomPublisher/Publishers.playground/Pages/Just.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | var cancellables = Set() 8 | 9 | // Outputを出力して後にすぐにCompletionを出力する 10 | run("Just") { 11 | let just = Just(1) 12 | just 13 | .sink(receiveCompletion: { finished in 14 | print("receivedCompletion: \(finished)") 15 | }, receiveValue: { value in 16 | print("receivedValue: \(value)") 17 | }).store(in: &cancellables) 18 | 19 | just 20 | .sink(receiveCompletion: { finished in 21 | print("receivedCompletion(2): \(finished)") 22 | }, receiveValue: { value in 23 | print("receivedValue(2): \(value)") 24 | }).store(in: &cancellables) 25 | } 26 | 27 | PlaygroundPage.current.needsIndefiniteExecution = true 28 | 29 | //: [Next](@next) 30 | -------------------------------------------------------------------------------- /Publishers/CustomPublisher/Publishers.playground/Sources/Helper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func run(_ name: String, action: () -> Void) { 4 | print("\n------Operator name:", name, "------\n") 5 | action() 6 | } 7 | 8 | extension Thread { 9 | public var number: Int { 10 | let desc = self.description 11 | let threadNumber = try! NSRegularExpression(pattern: "number = (\\d+)", options: .caseInsensitive) 12 | if let numberMatches = threadNumber.firstMatch(in: desc, range: NSMakeRange(0, desc.count)) { 13 | let s = NSString(string: desc).substring(with: numberMatches.range(at: 1)) 14 | return Int(s) ?? 0 15 | } 16 | return 0 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Publishers/CustomPublisher/Publishers.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Publishers/Publishers.playground/Pages/Deferred.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | var cancellables = Set() 8 | 9 | // 処理が完了した時にResultを引数に受け取るコールバック(promise)を呼び出す 10 | // DeferredはSubscribeされていないと内部の処理を実行しない 11 | run("Deferred without Subscribe") { 12 | _ = Deferred { 13 | Future { promise in 14 | sleep(1) 15 | print("executed") 16 | promise(.success(1)) 17 | } 18 | } 19 | } 20 | 21 | // 処理が完了した時にResultを引数に受け取るコールバック(promise)を呼び出す 22 | // DeferredはSubscribeされていないと内部の処理を実行しない 23 | run("Deferred with Subscribe") { 24 | Deferred { 25 | Future { promise in 26 | sleep(1) 27 | print("executed") 28 | promise(.success(2)) 29 | } 30 | } 31 | .sink(receiveCompletion: { finished in 32 | print("receivedCompletion: \(finished)") 33 | }, receiveValue: { value in 34 | print("receivedValue: \(value)") 35 | }).store(in: &cancellables) 36 | } 37 | 38 | PlaygroundPage.current.needsIndefiniteExecution = true 39 | 40 | //: [Next](@next) 41 | -------------------------------------------------------------------------------- /Publishers/Publishers.playground/Pages/Empty.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | var cancellables = Set() 8 | 9 | // Outputを出力しないですぐにComlpetion(Success)を出力する 10 | run("Empty") { 11 | Empty() 12 | .sink(receiveCompletion: { finished in 13 | print("receivedCompletion: \(finished)") 14 | }, receiveValue: { value in 15 | print("receivedValue: \(value)") 16 | }).store(in: &cancellables) 17 | } 18 | 19 | PlaygroundPage.current.needsIndefiniteExecution = true 20 | 21 | //: [Next](@next) 22 | -------------------------------------------------------------------------------- /Publishers/Publishers.playground/Pages/Fail.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | var cancellables = Set() 8 | 9 | // Outputを出力しないですぐにComlpetion(Failure)を出力する 10 | run("Fail") { 11 | struct SampleError: Error {} 12 | Fail(error: SampleError()) 13 | .sink(receiveCompletion: { finished in 14 | print("receivedCompletion: \(finished)") 15 | }, receiveValue: { value in 16 | print("receivedValue: \(value)") 17 | }).store(in: &cancellables) 18 | } 19 | 20 | PlaygroundPage.current.needsIndefiniteExecution = true 21 | 22 | //: [Next](@next) 23 | -------------------------------------------------------------------------------- /Publishers/Publishers.playground/Pages/Future.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | var cancellables = Set() 8 | 9 | // 処理が完了した時にResultを引数に受け取るコールバック(promise)を呼び出す 10 | // Outputを流したらすぐにCompletionを出力する 11 | // FutureはSubscribeされていないくても内部の処理を実行する 12 | run("Future with Subscribe") { 13 | Future { promise in 14 | sleep(1) 15 | print("executed") 16 | promise(.success(1)) 17 | } 18 | .sink(receiveCompletion: { finished in 19 | print("receivedCompletion: \(finished)") 20 | }, receiveValue: { value in 21 | print("receivedValue: \(value)") 22 | }).store(in: &cancellables) 23 | } 24 | 25 | // 処理が完了した時にResultを引数に受け取るコールバック(promise)を呼び出す 26 | // FutureはSubscribeされていないくても内部の処理を実行する 27 | run("Future without Subscribe") { 28 | _ = Future { promise in 29 | sleep(1) 30 | print("executed") 31 | promise(.success(2)) 32 | } 33 | } 34 | 35 | PlaygroundPage.current.needsIndefiniteExecution = true 36 | 37 | //: [Next](@next) 38 | -------------------------------------------------------------------------------- /Publishers/Publishers.playground/Pages/Just.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | var cancellables = Set() 8 | 9 | // Outputを出力して後にすぐにCompletionを出力する 10 | run("Just") { 11 | let just = Just(1) 12 | just 13 | .sink(receiveCompletion: { finished in 14 | print("receivedCompletion: \(finished)") 15 | }, receiveValue: { value in 16 | print("receivedValue: \(value)") 17 | }).store(in: &cancellables) 18 | 19 | just 20 | .sink(receiveCompletion: { finished in 21 | print("receivedCompletion(2): \(finished)") 22 | }, receiveValue: { value in 23 | print("receivedValue(2): \(value)") 24 | }).store(in: &cancellables) 25 | } 26 | 27 | PlaygroundPage.current.needsIndefiniteExecution = true 28 | 29 | //: [Next](@next) 30 | -------------------------------------------------------------------------------- /Publishers/Publishers.playground/Pages/Record.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import PlaygroundSupport 6 | 7 | var cancellables = Set() 8 | 9 | // OutputとCompletionを記録しておき 10 | // Subscribeされると全ての値を出力する 11 | // テスト目的で利用されることが多い 12 | run("Record") { 13 | let record = Record(output: [1,2,3,4], completion: .finished) 14 | record 15 | .sink(receiveCompletion: { finished in 16 | print("receivedCompletion: \(finished)") 17 | }, receiveValue: { value in 18 | print("receivedValue: \(value)") 19 | }) 20 | .store(in: &cancellables) 21 | } 22 | 23 | PlaygroundPage.current.needsIndefiniteExecution = true 24 | 25 | //: [Next](@next) 26 | -------------------------------------------------------------------------------- /Publishers/Publishers.playground/Sources/Helper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func run(_ name: String, action: () -> Void) { 4 | print("\n------Operator name:", name, "------\n") 5 | action() 6 | } 7 | 8 | extension Thread { 9 | public var number: Int { 10 | let desc = self.description 11 | let threadNumber = try! NSRegularExpression(pattern: "number = (\\d+)", options: .caseInsensitive) 12 | if let numberMatches = threadNumber.firstMatch(in: desc, range: NSMakeRange(0, desc.count)) { 13 | let s = NSString(string: desc).substring(with: numberMatches.range(at: 1)) 14 | return Int(s) ?? 0 15 | } 16 | return 0 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Publishers/Publishers.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # このリポジトリに含まれているもの 2 | 3 | ## Recommend 4 | 5 | これまでに参考にしたサイト、動画、本などを記載しています。 6 | もし良い記事やサイトなどご存知でしたら、ぜひ教えてください。 7 | 8 | ## 実行環境 9 | 10 | Xcode12 11 | iOS14 12 | 13 | ## Presentation 14 | 15 | iOSDC 登壇時に使用した資料です。 16 | 17 | スライドはこちら 18 | https://speakerdeck.com/shiz/sorosorocombine 19 | 20 | 動画はこちら 21 | https://www.youtube.com/watch?v=0wTld_ROx2Y&list=PLod2oSGQp3W4BV6sLUdMwlZD0NHt9mHP7&index=18 22 | 23 | ## Operators 24 | 25 | Operator の動作を確認するための Playground 集です。 26 | ソースコメントにそれぞれの特徴などを記載しています。 27 | 28 | ## Publishers 29 | 30 | Publisher の動作を確認するための Playground 集です。 31 | ソースコメントにそれぞれの特徴などを記載しています。 32 | 33 | ## Schedulers 34 | 35 | Scheduler の動作を確認するための Playground 集です。 36 | ソースコメントそれぞれの特徴などを記載しています。 37 | 38 | また、テスト時に Scheduler を Control する方法として 39 | Custom Scheduler を使用した例も含んでいます。 40 | 41 | ## SampleApp 42 | 43 | 下記の 3 つのサンプルアプリがあります。 44 | 45 | - UserRegistration(テキスト入力、バリデーションの検証) 46 | - CombineCollection(リスト表示、詳細画面遷移の検証) 47 | - ComplexUserRegistration(複数画面にまたがった場合の検証) 48 | 49 | その中に複数のパターンがあります。 50 | 51 | - UIKit 52 | - UIKit + Combine 53 | - SwiftUI + Combine 54 | 55 | ※ SwiftUICombineCollection は Grid の画像数が多いと URLSession のリクエストでクラッシュします。現在解決策を調査中です。 56 | (Simulator のみで起きるようです。) 57 | 58 | ``` 59 | -[SwiftUI.AccessibilityNode retain]: message sent to deallocated instance) 60 | ``` 61 | 62 | ## Timelane 63 | 64 | Instruments を活用したデバッグツールのインストール方法や簡単な使用方法を紹介しています。 65 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/API/BreedListLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedListLoader.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct BreedListLoader { 11 | let load: () -> AnyPublisher<[Breed], Error> 12 | } 13 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/API/DogImageListLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogImageListLoader.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct DogImageListLoader { 11 | let load: (BreedType) -> AnyPublisher<[DogImage], Error> 12 | } 13 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/API/ImageDataLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDataLoader.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct ImageDataLoader { 11 | let load: (URL) -> AnyPublisher 12 | } 13 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/API/ImageDataWebLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDataWebLoader.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | final class ImageDataWebLoader { 11 | private let queue = DispatchQueue(label: "ImageWebAPI") 12 | private let client: HTTPClient 13 | init(client: HTTPClient) { 14 | self.client = client 15 | } 16 | 17 | var loader: ImageDataLoader { 18 | ImageDataLoader { [self] url in 19 | client.send(request: URLRequest(url: url)) 20 | .subscribe(on: queue) 21 | .eraseToAnyPublisher() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/API/Shared/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | protocol HTTPClient { 11 | func send(request: URLRequest) -> AnyPublisher 12 | } 13 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/API/Shared/URLSessionHTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct URLSessionHTTPClient: HTTPClient { 11 | private let session: URLSession 12 | init(session: URLSession) { 13 | self.session = session 14 | } 15 | 16 | func send(request: URLRequest) -> AnyPublisher { 17 | session.dataTaskPublisher(for: request) 18 | .tryMap { (data, response) in 19 | guard let httpResponse = response as? HTTPURLResponse, 20 | (200...299).contains(httpResponse.statusCode) else { 21 | throw URLError(.badServerResponse) 22 | } 23 | return data 24 | } 25 | .eraseToAnyPublisher() 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | @UIApplicationMain 10 | class AppDelegate: UIResponder, UIApplicationDelegate { 11 | 12 | 13 | 14 | func application(_ application: UIApplication, 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, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 22 | // Called when a new scene session is being created. 23 | // Use this method to select a configuration to create the new scene with. 24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 25 | } 26 | 27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 28 | // Called when the user discards a scene session. 29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 31 | } 32 | 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/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 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/Models/Breed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Breed.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | typealias BreedType = String 10 | 11 | struct Breed: Equatable, Decodable, Hashable { 12 | let name: String 13 | let subBreeds: [Breed] 14 | 15 | var id: Self { self } 16 | } 17 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/Models/DogImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogImage.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | struct DogImage: Equatable, Decodable, Hashable { 10 | let imageURL: URL 11 | } 12 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CombineCollection 4 | // 5 | // Created by Shinzan Takata on 2020/07/19. 6 | // 7 | 8 | import UIKit 9 | 10 | class BreedListViewController: UIViewController { 11 | struct Item: Hashable { 12 | let id: String 13 | } 14 | 15 | enum Section { 16 | case main 17 | } 18 | 19 | private var dataSource: UICollectionViewDiffableDataSource! 20 | private var collectionView: UICollectionView! 21 | private let appearance = UICollectionLayoutListConfiguration.Appearance.plain 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | navigationItem.title = "List" 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/Views/BreedImagesGrid/BreedImagesGridViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedImagesGridViewModel.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | final class BreedImagesGridViewModel { 11 | @Published private(set) var dogImages: [DogImage] = [] 12 | @Published private(set) var isLoading: Bool = false 13 | @Published private(set) var error: Error? = nil 14 | 15 | private let loader: DogImageListLoader 16 | init(loader: DogImageListLoader) { 17 | self.loader = loader 18 | } 19 | 20 | func fetch(breedType: BreedType) { 21 | isLoading = true 22 | error = nil 23 | 24 | loader.load(breedType) 25 | .receive(on: DispatchQueue.main) 26 | .handleEvents( 27 | receiveOutput: { [weak self] _ in 28 | self?.isLoading = false 29 | }, 30 | receiveCompletion: { [weak self] finished in 31 | self?.isLoading = false 32 | if case .failure(let error) = finished { 33 | self?.error = error 34 | } 35 | } 36 | ) 37 | .replaceError(with: []) 38 | .assign(to: &$dogImages) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/Views/BreedImagesGrid/ImageCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCell.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import UIKit 9 | 10 | final class ImageCell: UICollectionViewCell { 11 | @Published var isLoading: Bool = false 12 | 13 | let imageView = UIImageView() 14 | 15 | var cancellables = Set() 16 | 17 | private lazy var loadingView = LoadingView() 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | configure() 22 | setupBindings() 23 | } 24 | 25 | required init?(coder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | override func prepareForReuse() { 30 | super.prepareForReuse() 31 | imageView.image = nil 32 | 33 | // reuse時に前のリクエストをキャンセルする 34 | cancellables = Set() 35 | } 36 | 37 | private func configure() { 38 | contentView.addSubview(imageView) 39 | imageView.translatesAutoresizingMaskIntoConstraints = false 40 | NSLayoutConstraint.activate([ 41 | imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 42 | imageView.topAnchor.constraint(equalTo: contentView.topAnchor), 43 | imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), 44 | imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 45 | ]) 46 | } 47 | 48 | private func setupBindings() { 49 | $isLoading.sink { [self] isLoading in 50 | if isLoading { 51 | startLoading() 52 | } else { 53 | stopLoading() 54 | } 55 | }.store(in: &cancellables) 56 | } 57 | 58 | private func startLoading() { 59 | contentView.addSubview(loadingView) 60 | loadingView.frame = contentView.bounds 61 | loadingView.start() 62 | } 63 | 64 | private func stopLoading() { 65 | loadingView.stop() 66 | loadingView.removeFromSuperview() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/Views/BreedList/BreedListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedListViewModel.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | final class BreedListViewModel { 11 | @Published private(set) var breeds: [DisplayBreed] = [] 12 | @Published private(set) var isLoading: Bool = false 13 | @Published private(set) var error: Error? = nil 14 | 15 | private let loader: BreedListLoader 16 | init(loader: BreedListLoader) { 17 | self.loader = loader 18 | } 19 | 20 | func fetchList() { 21 | isLoading = true 22 | error = nil 23 | 24 | loader.load() 25 | .map { $0.sorted(by: { $0.name < $1.name }) } 26 | .map(makeDisplayModels(from:)) 27 | .receive(on: DispatchQueue.main) 28 | .handleEvents( 29 | receiveOutput: { [weak self] _ in 30 | self?.isLoading = false 31 | }, 32 | receiveCompletion: { [weak self] finished in 33 | self?.isLoading = false 34 | if case .failure(let error) = finished { 35 | self?.error = error 36 | } 37 | } 38 | ) 39 | .replaceError(with: []) 40 | .assign(to: &$breeds) 41 | } 42 | 43 | func makeDisplayModels(from breeds: [Breed]) -> [DisplayBreed] { 44 | breeds.map(makeDisplayModel(from:)) 45 | } 46 | 47 | private func makeDisplayModel(from breed: Breed) -> DisplayBreed { 48 | let children = breed.subBreeds 49 | .map { DisplayBreed(name: "\(breed.name)/\($0.name)", displayName: $0.name.uppercased()) 50 | } 51 | let allKindsItem = DisplayBreed(name: breed.name, displayName: "\(breed.name.uppercased())の全種別") 52 | return DisplayBreed(name: breed.name, displayName: breed.name.uppercased(), 53 | subBreeds: [allKindsItem] + children) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/Views/BreedList/DisplayBreed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayBreed.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | struct DisplayBreed: Hashable { 10 | let name: String 11 | let displayName: String 12 | let subBreeds: [DisplayBreed] 13 | init(name: String, displayName: String, 14 | subBreeds: [DisplayBreed] = []) { 15 | self.name = name 16 | self.displayName = displayName 17 | self.subBreeds = subBreeds 18 | } 19 | } 20 | 21 | extension DisplayBreed: Identifiable { 22 | var id: Self { self } 23 | 24 | static var anyBreed: DisplayBreed { 25 | DisplayBreed(name: "child1", displayName: "child1") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollection/Views/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewHelper.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | final class LoadingView: UIView { 10 | private lazy var indicator: UIActivityIndicatorView = { 11 | let indicator = UIActivityIndicatorView(style: .large) 12 | indicator.hidesWhenStopped = true 13 | addSubview(indicator) 14 | indicator.translatesAutoresizingMaskIntoConstraints = false 15 | NSLayoutConstraint.activate([ 16 | indicator.centerXAnchor.constraint(equalTo: centerXAnchor), 17 | indicator.centerYAnchor.constraint(equalTo: centerYAnchor) 18 | ]) 19 | return indicator 20 | }() 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | setup() 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | func start() { 32 | indicator.startAnimating() 33 | } 34 | 35 | func stop() { 36 | indicator.stopAnimating() 37 | } 38 | 39 | private func setup() { 40 | backgroundColor = UIColor.lightGray.withAlphaComponent(0.5) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollectionTests/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // CombineCollectionTests 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | @testable import CombineCollection 10 | 11 | extension BreedListLoader { 12 | static let emptyLoader = BreedListLoader { 13 | Empty().setFailureType(to: Error.self) 14 | .eraseToAnyPublisher() 15 | } 16 | 17 | static func loader(_ response: [Breed]) -> BreedListLoader { 18 | BreedListLoader { 19 | Just(response) 20 | .setFailureType(to: Error.self) 21 | .eraseToAnyPublisher() 22 | } 23 | } 24 | 25 | static func error(_ error: Error) -> BreedListLoader { 26 | BreedListLoader { 27 | Fail(error: error) 28 | .eraseToAnyPublisher() 29 | } 30 | } 31 | } 32 | 33 | var anyBreed: Breed { 34 | Breed(name: "\(UUID().uuidString)", subBreeds: []) 35 | } 36 | 37 | extension Breed { 38 | var toDisplayBreed: DisplayBreed { 39 | let allKind = subBreedForAllKind(name: name) 40 | guard self.subBreeds.isEmpty else { 41 | return DisplayBreed(name: name, displayName: name, 42 | subBreeds: [allKind]) 43 | } 44 | let subBreeds = self.subBreeds.map { 45 | DisplayBreed( 46 | name: $0.name, displayName: $0.name, subBreeds: []) 47 | } 48 | return DisplayBreed(name: name, displayName: name, 49 | subBreeds: [allKind] + subBreeds) 50 | } 51 | 52 | private func subBreedForAllKind(name: String) -> DisplayBreed { 53 | DisplayBreed(name: name, displayName: "\(name)の全種別") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sampleapp/CombineCollection/CombineCollectionTests/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 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistration/ComplexUserRegistration.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistration/ComplexUserRegistration.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistration/ComplexUserRegistration/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ComplexUserRegistration 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | @main 10 | class AppDelegate: UIResponder, UIApplicationDelegate { 11 | 12 | 13 | 14 | func application(_ application: UIApplication, 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, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 22 | // Called when a new scene session is being created. 23 | // Use this method to select a configuration to create the new scene with. 24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 25 | } 26 | 27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 28 | // Called when the user discards a scene session. 29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 31 | } 32 | 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistration/ComplexUserRegistration/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistration/ComplexUserRegistration/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistration/ComplexUserRegistration/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistration/ComplexUserRegistration/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 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistration/ComplexUserRegistration/Models/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState.swift 3 | // ComplexUserRegistration 4 | // 5 | // 6 | 7 | import Combine 8 | 9 | @dynamicMemberLookup 10 | final class AppState { 11 | @Published var registrationInformation: RegistrationInformation = .initial 12 | @Published var step: Step = .userName 13 | 14 | subscript(dynamicMember keyPath: WritableKeyPath) -> A { 15 | get { self.registrationInformation[keyPath: keyPath] } 16 | set { self.registrationInformation[keyPath: keyPath] = newValue } 17 | } 18 | 19 | subscript(dynamicMember keyPath: WritableKeyPath) -> A { 20 | get { self.registrationInformation.address[keyPath: keyPath] } 21 | set { self.registrationInformation.address[keyPath: keyPath] = newValue } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistration/ComplexUserRegistration/Models/RegistrationInformation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegistrationInformation.swift 3 | // ComplexUserRegistration 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | typealias UserName = String 10 | typealias Password = String 11 | 12 | struct RegistrationInformation { 13 | var userName: UserName 14 | var password: Password 15 | var address: Address 16 | 17 | static let initial: Self = .init(userName: "", password: "", address: .initial) 18 | } 19 | 20 | // MARK: - Address 21 | 22 | struct Address { 23 | var zipcode: String 24 | var prefecture: String 25 | var city: String 26 | var other: String 27 | static let initial: Self = .init(zipcode: "", prefecture: "", city: "", other: "") 28 | } 29 | 30 | extension Address { 31 | var displayed: String { 32 | "\(zipcode)\n\(prefecture)\(city)\(other)" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistration/ComplexUserRegistration/Models/Step.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Step.swift 3 | // ComplexUserRegistration 4 | // 5 | // 6 | 7 | enum Step { 8 | case userName 9 | case address 10 | case password 11 | case completion 12 | } 13 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistrationSwiftUI/ComplexUserRegistrationSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistrationSwiftUI/ComplexUserRegistrationSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistrationSwiftUI/ComplexUserRegistrationSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistrationSwiftUI/ComplexUserRegistrationSwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistrationSwiftUI/ComplexUserRegistrationSwiftUI/Completion/CompletionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompletionView.swift 3 | // ComplexUserRegistration 4 | // 5 | // 6 | 7 | import SwiftUI 8 | 9 | struct CompletionView: View { 10 | @Binding var restart: Bool 11 | @ObservedObject var state: AppState 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 16) { 15 | Text("ユーザー名:\n\(state.userName)") 16 | Text("パスワード:\n\(state.password)") 17 | Text("住所:\n\(state.address.displayed)") 18 | Button("最初から") { restart = true } 19 | .padding() 20 | .frame(maxWidth: .infinity, alignment: .center) 21 | .background(RoundedRectangle(cornerRadius: 20) 22 | .stroke(Color.blue, lineWidth: 5)) 23 | .animation(nil) 24 | Spacer() 25 | } 26 | .onDisappear { restart = false } 27 | } 28 | } 29 | 30 | struct CompletionView_Previews: PreviewProvider { 31 | static var previews: some View { 32 | let state = AppState() 33 | state.registrationInformation = .mock 34 | return CompletionView(restart: .constant(false), 35 | state: state) 36 | } 37 | } 38 | 39 | extension Address { 40 | static let mock: Self = .init(zipcode: "11111111", 41 | prefecture: "東京都", city: "千代田区", 42 | other: "中央1−1−1") 43 | } 44 | 45 | extension RegistrationInformation { 46 | static let mock: Self = .init(userName: "Test", password: "password", address: .mock) 47 | } 48 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistrationSwiftUI/ComplexUserRegistrationSwiftUI/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 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistrationSwiftUI/ComplexUserRegistrationSwiftUI/Models/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState.swift 3 | // ComplexUserRegistration 4 | // 5 | // 6 | 7 | import Combine 8 | import SwiftUI 9 | 10 | @dynamicMemberLookup 11 | final class AppState: ObservableObject { 12 | @Published var registrationInformation: RegistrationInformation = .initial 13 | @Published var step: Step = .userName 14 | 15 | @Published var passwordConfirm: String = "" 16 | 17 | subscript(dynamicMember keyPath: WritableKeyPath) -> A { 18 | get { self.registrationInformation[keyPath: keyPath] } 19 | set { self.registrationInformation[keyPath: keyPath] = newValue } 20 | } 21 | 22 | subscript(dynamicMember keyPath: WritableKeyPath) -> A { 23 | get { self.registrationInformation.address[keyPath: keyPath] } 24 | set { self.registrationInformation.address[keyPath: keyPath] = newValue } 25 | } 26 | 27 | func binding( 28 | get: @escaping (AppState) -> Value, 29 | set: @escaping (Value) -> Void 30 | ) -> Binding { 31 | Binding(get: { get(self) }, set: set) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistrationSwiftUI/ComplexUserRegistrationSwiftUI/Models/RegistrationInformation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegistrationInformation.swift 3 | // ComplexUserRegistration 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | typealias UserName = String 10 | typealias Password = String 11 | 12 | struct RegistrationInformation { 13 | var userName: UserName 14 | var password: Password 15 | var address: Address 16 | 17 | static let initial: Self = .init(userName: "", password: "", address: .initial) 18 | } 19 | 20 | // MARK: - Address 21 | 22 | struct Address { 23 | var zipcode: String 24 | var prefecture: String 25 | var city: String 26 | var other: String 27 | static let initial: Self = .init(zipcode: "", prefecture: "", city: "", other: "") 28 | } 29 | 30 | extension Address { 31 | var displayed: String { 32 | "\(zipcode)\n\(prefecture)\(city)\(other)" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistrationSwiftUI/ComplexUserRegistrationSwiftUI/Models/Step.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Step.swift 3 | // ComplexUserRegistration 4 | // 5 | // 6 | 7 | enum Step { 8 | case userName 9 | case address 10 | case password 11 | case completion 12 | } 13 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistrationSwiftUI/ComplexUserRegistrationSwiftUI/Password/PasswordView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordView.swift 3 | // ComplexUserRegistration 4 | // 5 | // 6 | 7 | import Combine 8 | import SwiftUI 9 | 10 | struct PasswordView: View { 11 | @ObservedObject var state: AppState 12 | 13 | var password: Binding { 14 | state.binding( 15 | get: \.password, 16 | set: { state[keyPath: \AppState.password] = $0 } 17 | ) 18 | } 19 | 20 | var passwordConfirm: Binding { 21 | state.binding( 22 | get: \.passwordConfirm, 23 | set: { state.passwordConfirm = $0 } 24 | ) 25 | } 26 | 27 | var body: some View { 28 | VStack(alignment: .center) { 29 | Text("パスワードを入力してください") 30 | SecureField("パスワード", text: password) 31 | .textFieldStyle(RoundedBorderTextFieldStyle()) 32 | .keyboardType(.alphabet) 33 | .padding() 34 | SecureField("パスワード確認", text: passwordConfirm) 35 | .textFieldStyle(RoundedBorderTextFieldStyle()) 36 | .keyboardType(.alphabet) 37 | .padding() 38 | Spacer() 39 | } 40 | } 41 | } 42 | 43 | struct PasswordView_Previews: PreviewProvider { 44 | static var previews: some View { 45 | return PasswordView(state: AppState()) 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistrationSwiftUI/ComplexUserRegistrationSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sampleapp/ComplexUserRegistrationSwiftUI/ComplexUserRegistrationSwiftUI/UserName/UserNameView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserNameView.swift 3 | // ComplexUserRegistration 4 | // 5 | // 6 | 7 | import Combine 8 | import SwiftUI 9 | 10 | struct UserNameView: View { 11 | @ObservedObject var state: AppState 12 | 13 | var userName: Binding { 14 | state.binding( 15 | get: \.userName, 16 | set: { state[keyPath: \AppState.userName] = $0 }) 17 | } 18 | 19 | var body: some View { 20 | VStack(alignment: .center) { 21 | Text("ユーザー名を入力してください") 22 | TextField("ユーザー名", text: userName) 23 | .textFieldStyle(RoundedBorderTextFieldStyle()) 24 | .padding() 25 | Spacer() 26 | } 27 | } 28 | } 29 | 30 | struct UserNameView_Previews: PreviewProvider { 31 | static var previews: some View { 32 | UserNameView(state: AppState()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/API/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | protocol HTTPClient { 11 | func send(request: URLRequest) -> AnyPublisher 12 | } 13 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/API/ImageDataWebLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDataWebLoader.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | final class ImageDataWebLoader { 11 | private let queue: DispatchQueue 12 | private let client: HTTPClient 13 | init(client: HTTPClient, 14 | queue: DispatchQueue = DispatchQueue(label: "ImageDataWebLoaderQueue", attributes: .concurrent)) { 15 | self.client = client 16 | self.queue = queue 17 | } 18 | 19 | func load(_ url: URL) -> AnyPublisher { 20 | client.send(request: URLRequest(url: url)) 21 | .subscribe(on: queue) 22 | .eraseToAnyPublisher() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/API/URLSessionHTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct URLSessionHTTPClient: HTTPClient { 11 | private let session: URLSession 12 | init(session: URLSession) { 13 | self.session = session 14 | } 15 | 16 | func send(request: URLRequest) -> AnyPublisher { 17 | session.dataTaskPublisher(for: request) 18 | .tryMap { (data, response) in 19 | guard let httpResponse = response as? HTTPURLResponse, 20 | (200...299).contains(httpResponse.statusCode) else { 21 | throw URLError(.badServerResponse) 22 | } 23 | return data 24 | } 25 | .eraseToAnyPublisher() 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import SwiftUI 8 | 9 | struct ContentView: View { 10 | @Environment(\.injected) var container: DIContainer 11 | 12 | var body: some View { 13 | BreedListView() 14 | .environment(\.injected, container) 15 | } 16 | } 17 | 18 | struct ContentView_Previews: PreviewProvider { 19 | static var previews: some View { 20 | ContentView() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/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 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/Models/Breed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Breed.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | typealias BreedType = String 10 | 11 | struct Breed: Hashable, Equatable, Decodable { 12 | let name: String 13 | let subBreeds: [Breed] 14 | } 15 | 16 | extension Breed: Identifiable { 17 | var id: Self { self } 18 | } 19 | 20 | extension Breed { 21 | static var anyBreed: Breed { 22 | let anyID = UUID().uuidString 23 | return Breed(name: "test\(anyID)", subBreeds: []) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/Models/DogImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogImage.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | struct DogImage: Equatable, Decodable, Hashable { 10 | let imageURL: URL 11 | } 12 | 13 | extension DogImage: Identifiable { 14 | var id: Self { self } 15 | } 16 | 17 | extension DogImage { 18 | static var anyDogImage: DogImage { 19 | DogImage(imageURL: URL(string: "https://\(UUID().uuidString).image.com")!) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/SwiftUICombineCollectionApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUICombineCollectionApp.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import SwiftUI 8 | 9 | @main 10 | struct SwiftUICombineCollectionApp: App { 11 | private var container = DIContainer.defaultValue 12 | 13 | var body: some Scene { 14 | WindowGroup { 15 | ContentView() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/Views/BreedImagesGrid/BreedImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogImage.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import SwiftUI 9 | 10 | struct BreedImageView: View { 11 | @ObservedObject var viewModel: BreedImageViewModel 12 | 13 | init(viewModel: BreedImageViewModel) { 14 | self.viewModel = viewModel 15 | } 16 | 17 | var body: some View { 18 | content 19 | .onAppear { 20 | viewModel.fetch() 21 | } 22 | .onDisappear { 23 | viewModel.cancel() 24 | } 25 | } 26 | 27 | @ViewBuilder 28 | private var content: some View { 29 | switch (viewModel.imageData, viewModel.error) { 30 | case (.none, .none): 31 | ProgressView("loading.....") 32 | case (let .some(data), .none): 33 | Image(uiImage: UIImage(data: data)!) 34 | .resizable() 35 | .aspectRatio(1, contentMode: .fit) 36 | .clipped() 37 | default: 38 | Image(systemName: "xmark.octagon.fill") 39 | } 40 | } 41 | } 42 | 43 | struct DogImageView_Previews: PreviewProvider { 44 | static var previews: some View { 45 | BreedImageView(viewModel: .init(url: URL(string: "https://any-url.com")!, loader: { 46 | _ in Just(UIImage(systemName: "xmark.octagon.fill")!.pngData()!) 47 | .setFailureType(to: Error.self).eraseToAnyPublisher() 48 | })) 49 | } 50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/Views/BreedImagesGrid/BreedImageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedImageViewModel.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | import SwiftUI 10 | 11 | final class BreedImageViewModel: ObservableObject { 12 | @Published var imageData: Data? 13 | @Published var error: Error? 14 | private var cancellable: AnyCancellable? 15 | 16 | private let url: URL 17 | private let loader: ImageDataLoader 18 | init(url: URL, loader: @escaping ImageDataLoader) { 19 | self.url = url 20 | self.loader = loader 21 | } 22 | 23 | func fetch() { 24 | cancellable = loader(url) 25 | .receiveOnMainQueue() 26 | // .receive(on: DispatchQueue.main) 27 | .sink { finished in 28 | if case .failure(let error) = finished { 29 | self.imageData = nil 30 | self.error = error 31 | } 32 | } 33 | receiveValue: { value in 34 | self.imageData = value 35 | self.error = nil 36 | } 37 | } 38 | 39 | func cancel() { 40 | cancellable?.cancel() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/Views/BreedImagesGrid/BreedImagesGridViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedImagesGridViewModel.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | import SwiftUI 10 | 11 | final class BreedImagesGridViewModel: ObservableObject { 12 | @Published var dogImages: [DogImage] = [] 13 | 14 | func fetch(breedType: BreedType, using loader: DogImageListLoader) { 15 | loader(breedType) 16 | .replaceError(with: []) 17 | .receiveOnMainQueue() 18 | .assign(to: &$dogImages) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/Views/BreedImagesGrid/ImageLoaderCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageLoader.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import SwiftUI 8 | import UIKit 9 | import Combine 10 | 11 | final class ImageLoaderCache { 12 | @Environment(\.injected.loaders) var dataLoaders: DIContainer.Loaders 13 | static let shared = ImageLoaderCache() 14 | 15 | private var loaders: NSCache = NSCache() 16 | 17 | func loaderFor(url: URL) -> BreedImageViewModel { 18 | let key = NSString(string: url.absoluteString) 19 | if let loader = loaders.object(forKey: key) { 20 | return loader 21 | } else { 22 | let loader = BreedImageViewModel(url: url, 23 | loader: dataLoaders.imageDataLoader) 24 | loaders.setObject(loader, forKey: key) 25 | return loader 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/Views/BreedList/BreedListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedListViewModel.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | import SwiftUI 10 | 11 | final class BreedListViewModel: ObservableObject { 12 | @Published var breeds: [DisplayBreed] = [] 13 | @Published var isLoading: Bool = false 14 | @Published var expansionStates: [DisplayBreed: Bool] = [:] 15 | 16 | func fetchList(using loader: BreedListLoader) { 17 | isLoading = true 18 | loader() 19 | .map { $0.sorted(by: { $0.name < $1.name }) } 20 | .map(makeDisplayModels(from:)) 21 | .receiveOnMainQueue() 22 | // .receive(on: DispatchQueue.main) 23 | .replaceError(with: []) 24 | .handleEvents(receiveOutput: { _ in self.isLoading = false }) 25 | .assign(to: &$breeds) 26 | } 27 | 28 | func makeDisplayModels(from breeds: [Breed]) -> [DisplayBreed] { 29 | breeds.map(makeDisplayModel(from:)) 30 | } 31 | 32 | private func makeDisplayModel(from breed: Breed) -> DisplayBreed { 33 | let children = breed.subBreeds 34 | .map { DisplayBreed(name: "\(breed.name)/\($0.name)", displayName: $0.name.uppercased()) 35 | } 36 | let allKindsItem = DisplayBreed(name: breed.name, displayName: "\(breed.name.uppercased())の全種別") 37 | return DisplayBreed(name: breed.name, displayName: breed.name.uppercased(), 38 | subBreeds: [allKindsItem] + children) 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/Views/BreedList/BreedRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedRow.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import SwiftUI 8 | 9 | struct BreedRow: View { 10 | @Environment(\.colorScheme) var colorScheme 11 | 12 | let breed: DisplayBreed 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 8) { 15 | content 16 | } 17 | } 18 | 19 | @ViewBuilder 20 | var content: some View { 21 | if breed.subBreeds.isEmpty { 22 | HStack { 23 | Text(breed.displayName) 24 | .font(.headline) 25 | .padding([.leading], 8) 26 | Spacer() 27 | Image(systemName: "chevron.right") 28 | .foregroundColor(colorScheme == .dark ? .white : .black) 29 | } 30 | } else { 31 | HStack { 32 | Text(breed.displayName) 33 | .font(.headline) 34 | } 35 | } 36 | } 37 | } 38 | 39 | struct BreedRow_Previews: PreviewProvider { 40 | static var previews: some View { 41 | let breed = Breed.anyBreed 42 | BreedRow(breed: DisplayBreed(name: breed.name, displayName: breed.name, subBreeds: [])) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sampleapp/SwiftUICombineCollection/SwiftUICombineCollection/Views/BreedList/DisplayBreed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayBreed.swift 3 | // SwiftUICombineCollection 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | struct DisplayBreed: Hashable { 10 | let name: String 11 | let displayName: String 12 | let subBreeds: [DisplayBreed] 13 | init(name: String, displayName: String, 14 | subBreeds: [DisplayBreed] = []) { 15 | self.name = name 16 | self.displayName = displayName 17 | self.subBreeds = subBreeds 18 | } 19 | } 20 | 21 | extension DisplayBreed: Identifiable { 22 | var id: Self { self } 23 | 24 | static var anyBreed: DisplayBreed { 25 | DisplayBreed(name: "child1", displayName: "child1") 26 | } 27 | 28 | #if DEBUG 29 | static var anyBreeds: [DisplayBreed] { 30 | [DisplayBreed( 31 | name: "parent", displayName: "parent", 32 | subBreeds: [ 33 | DisplayBreed(name: "child1", displayName: "child1"), 34 | DisplayBreed(name: "child2", displayName: "child2"), 35 | DisplayBreed(name: "child3", displayName: "child3") 36 | ] 37 | )] 38 | } 39 | #endif 40 | } 41 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistration/UserRegistration.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistration/UserRegistration.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistration/UserRegistration/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // UserRegistration 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | @UIApplicationMain 10 | class AppDelegate: UIResponder, UIApplicationDelegate { 11 | 12 | 13 | 14 | func application(_ application: UIApplication, 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, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 22 | // Called when a new scene session is being created. 23 | // Use this method to select a configuration to create the new scene with. 24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 25 | } 26 | 27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 28 | // Called when the user discards a scene session. 29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 31 | } 32 | 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistration/UserRegistration/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistration/UserRegistration/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistration/UserRegistration/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistration/UserRegistration/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 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombine/UserRegistrationCombine.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombine/UserRegistrationCombine.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombine/UserRegistrationCombine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "TimelaneCombine", 6 | "repositoryURL": "https://github.com/icanzilb/TimelaneCombine", 7 | "state": { 8 | "branch": null, 9 | "revision": "d97f27b600de3b5a2ad20f610d67c6eeac380300", 10 | "version": "1.0.10" 11 | } 12 | }, 13 | { 14 | "package": "TimelaneCore", 15 | "repositoryURL": "https://github.com/icanzilb/TimelaneCore", 16 | "state": { 17 | "branch": null, 18 | "revision": "35437d370a6127d14f728065b3d4e3e9cf65e6a0", 19 | "version": "1.0.12" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombine/UserRegistrationCombine/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // UserRegistrationCombine 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | @UIApplicationMain 10 | class AppDelegate: UIResponder, UIApplicationDelegate { 11 | 12 | 13 | 14 | func application(_ application: UIApplication, 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, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 22 | // Called when a new scene session is being created. 23 | // Use this method to select a configuration to create the new scene with. 24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 25 | } 26 | 27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 28 | // Called when the user discards a scene session. 29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 31 | } 32 | 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombine/UserRegistrationCombine/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombine/UserRegistrationCombine/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombine/UserRegistrationCombine/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 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombine/UserRegistrationCombine/UIControl+Combine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIControl+Combine.swift 3 | // UserRegistrationCombine 4 | // 5 | // 6 | 7 | import Combine 8 | import UIKit 9 | 10 | extension UIControl { 11 | final class Subscription: Combine.Subscription 12 | where Target.Input == Void { 13 | private var subscriber: Target? 14 | 15 | init(subscriber: Target, event: UIControl.Event) { 16 | self.subscriber = subscriber 17 | } 18 | 19 | func request(_ demand: Subscribers.Demand) {} 20 | 21 | func cancel() { 22 | subscriber = nil 23 | } 24 | 25 | @objc func eventHandler() { 26 | _ = subscriber?.receive(()) 27 | } 28 | } 29 | 30 | struct Publisher: Combine.Publisher { 31 | typealias Output = Void 32 | typealias Failure = Never 33 | 34 | let control: UIControl 35 | let event: Event 36 | 37 | func receive(subscriber: S) where S : Subscriber, 38 | S.Failure == Failure, 39 | S.Input == Output { 40 | let subscription = UIControl.Subscription(subscriber: subscriber, 41 | event: event) 42 | subscriber.receive(subscription: subscription) 43 | control.addTarget(subscription, 44 | action: #selector(subscription.eventHandler), 45 | for: event) 46 | } 47 | } 48 | } 49 | 50 | extension UIControl { 51 | func publisher(for event: Event) -> UIControl.Publisher { 52 | return UIControl.Publisher(control: self, event: event) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombineSwiftUI/UserRegistrationCombineSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombineSwiftUI/UserRegistrationCombineSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombineSwiftUI/UserRegistrationCombineSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombineSwiftUI/UserRegistrationCombineSwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombineSwiftUI/UserRegistrationCombineSwiftUI/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 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombineSwiftUI/UserRegistrationCombineSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sampleapp/UserRegistrationCombineSwiftUI/UserRegistrationCombineSwiftUI/UserRegistrationCombineSwiftUIApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserRegistrationCombineSwiftUIApp.swift 3 | // UserRegistrationCombineSwiftUI 4 | // 5 | // 6 | 7 | import SwiftUI 8 | 9 | @main 10 | struct UserRegistrationCombineSwiftUIApp: App { 11 | var body: some Scene { 12 | WindowGroup { 13 | ContentView() 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/API/BreedListLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedListLoader.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct BreedListLoader { 11 | let load: () -> AnyPublisher<[Breed], Error> 12 | } 13 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/API/DogImageListLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogImageListLoader.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct DogImageListLoader { 11 | let load: (BreedType) -> AnyPublisher<[DogImage], Error> 12 | } 13 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/API/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | protocol HTTPClient { 11 | func send(request: URLRequest) -> AnyPublisher 12 | } 13 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/API/ImageDataLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDataLoader.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct ImageDataLoader { 11 | let load: (URL) -> AnyPublisher 12 | } 13 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/API/ImageDataWebLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDataWebLoader.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | final class ImageDataWebLoader { 11 | private let queue = DispatchQueue(label: "ImageWebAPI") 12 | private let client: HTTPClient 13 | init(client: HTTPClient) { 14 | self.client = client 15 | } 16 | 17 | var loader: ImageDataLoader { 18 | ImageDataLoader { [self] url in 19 | client.send(request: URLRequest(url: url)) 20 | .subscribe(on: queue) 21 | .eraseToAnyPublisher() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/API/URLSessionHTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct URLSessionHTTPClient: HTTPClient { 11 | private let session: URLSession 12 | init(session: URLSession) { 13 | self.session = session 14 | } 15 | 16 | func send(request: URLRequest) -> AnyPublisher { 17 | session.dataTaskPublisher(for: request) 18 | .tryMap { (data, response) in 19 | guard let httpResponse = response as? HTTPURLResponse, 20 | httpResponse.statusCode == 200 else { 21 | throw URLError(.badServerResponse) 22 | } 23 | return data 24 | } 25 | .eraseToAnyPublisher() 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | @UIApplicationMain 10 | class AppDelegate: UIResponder, UIApplicationDelegate { 11 | 12 | 13 | 14 | func application(_ application: UIApplication, 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, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 22 | // Called when a new scene session is being created. 23 | // Use this method to select a configuration to create the new scene with. 24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 25 | } 26 | 27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 28 | // Called when the user discards a scene session. 29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 31 | } 32 | 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/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 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/Models/Breed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Breed.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | typealias BreedType = String 10 | 11 | struct Breed: Equatable, Decodable, Hashable { 12 | let name: String 13 | let subBreeds: [Breed] 14 | 15 | var id: Self { self } 16 | } 17 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/Models/DogImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogImage.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | struct DogImage: Equatable, Decodable, Hashable { 10 | let imageURL: URL 11 | } 12 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CombineCollection 4 | // 5 | // Created by Shinzan Takata on 2020/07/19. 6 | // 7 | 8 | import UIKit 9 | 10 | class BreedListViewController: UIViewController { 11 | struct Item: Hashable { 12 | let id: String 13 | } 14 | 15 | enum Section { 16 | case main 17 | } 18 | 19 | private var dataSource: UICollectionViewDiffableDataSource! 20 | private var collectionView: UICollectionView! 21 | private let appearance = UICollectionLayoutListConfiguration.Appearance.plain 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | navigationItem.title = "List" 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/Views/BreedImagesGrid/BreedImagesGridViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedImagesGridViewModel.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | final class BreedImagesGridViewModel { 11 | @Published private(set) var dogImages: [DogImage] = [] 12 | @Published private(set) var isLoading: Bool = false 13 | @Published private(set) var error: Error? = nil 14 | 15 | private let loader: DogImageListLoader 16 | private let scheduler: AnySchedulerOf 17 | init(loader: DogImageListLoader, 18 | scheduler: AnySchedulerOf) { 19 | self.loader = loader 20 | self.scheduler = scheduler 21 | } 22 | 23 | func fetch(breedType: BreedType) { 24 | isLoading = true 25 | error = nil 26 | 27 | loader.load(breedType) 28 | .replaceError(with: []) 29 | .receive(on: self.scheduler) 30 | .handleEvents( 31 | receiveOutput: { [weak self] _ in 32 | self?.isLoading = false 33 | }, 34 | receiveCompletion: { [weak self] finished in 35 | self?.isLoading = false 36 | if case .failure(let error) = finished { 37 | self?.error = error 38 | } 39 | } 40 | ) 41 | .assign(to: &$dogImages) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/Views/BreedList/BreedListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedListViewModel.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | final class BreedListViewModel { 11 | @Published private(set) var breeds: [DisplayBreed] = [] 12 | @Published private(set) var isLoading: Bool = false 13 | @Published private(set) var error: Error? = nil 14 | 15 | private let loader: BreedListLoader 16 | init(loader: BreedListLoader) { 17 | self.loader = loader 18 | } 19 | 20 | func fetchList() { 21 | isLoading = true 22 | error = nil 23 | loader.load() 24 | .map { $0.sorted(by: { $0.name < $1.name }) } 25 | .map(makeDisplayModels(from:)) 26 | .receive(on: DispatchQueue.main) 27 | .handleEvents( 28 | receiveOutput: { [weak self] _ in 29 | self?.isLoading = false 30 | }, 31 | receiveCompletion: { [weak self] finished in 32 | self?.isLoading = false 33 | if case .failure(let error) = finished { 34 | self?.error = error 35 | } 36 | } 37 | ) 38 | .replaceError(with: []) 39 | .assign(to: &$breeds) 40 | } 41 | 42 | func makeDisplayModels(from breeds: [Breed]) -> [DisplayBreed] { 43 | breeds.map(makeDisplayModel(from:)) 44 | } 45 | 46 | private func makeDisplayModel(from breed: Breed) -> DisplayBreed { 47 | let children = breed.subBreeds 48 | .map { DisplayBreed(name: "\(breed.name)/\($0.name)", displayName: $0.name.uppercased()) 49 | } 50 | let allKindsItem = DisplayBreed(name: breed.name, displayName: "\(breed.name.uppercased())の全種別") 51 | return DisplayBreed(name: breed.name, displayName: breed.name.uppercased(), 52 | subBreeds: [allKindsItem] + children) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/Views/BreedList/DisplayBreed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayBreed.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | struct DisplayBreed: Hashable { 10 | let name: String 11 | let displayName: String 12 | let subBreeds: [DisplayBreed] 13 | init(name: String, displayName: String, 14 | subBreeds: [DisplayBreed] = []) { 15 | self.name = name 16 | self.displayName = displayName 17 | self.subBreeds = subBreeds 18 | } 19 | } 20 | 21 | extension DisplayBreed: Identifiable { 22 | var id: Self { self } 23 | 24 | static var anyBreed: DisplayBreed { 25 | DisplayBreed(name: "child1", displayName: "child1") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollection/Views/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingView.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | final class LoadingView: UIView { 10 | private lazy var indicator: UIActivityIndicatorView = { 11 | let indicator = UIActivityIndicatorView(style: .large) 12 | indicator.hidesWhenStopped = true 13 | addSubview(indicator) 14 | indicator.translatesAutoresizingMaskIntoConstraints = false 15 | NSLayoutConstraint.activate([ 16 | indicator.centerXAnchor.constraint(equalTo: centerXAnchor), 17 | indicator.centerYAnchor.constraint(equalTo: centerYAnchor) 18 | ]) 19 | return indicator 20 | }() 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | setup() 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | func start() { 32 | indicator.startAnimating() 33 | } 34 | 35 | func stop() { 36 | indicator.stopAnimating() 37 | } 38 | 39 | private func setup() { 40 | backgroundColor = UIColor.lightGray.withAlphaComponent(0.5) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_AnySchedular/CombineCollection/CombineCollectionTests/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 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/API/BreedListLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedListLoader.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct BreedListLoader { 11 | let load: () -> AnyPublisher<[Breed], Error> 12 | } 13 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/API/DogImageListLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogImageListLoader.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct DogImageListLoader { 11 | let load: (BreedType) -> AnyPublisher<[DogImage], Error> 12 | } 13 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/API/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | protocol HTTPClient { 11 | func send(request: URLRequest) -> AnyPublisher 12 | } 13 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/API/ImageDataLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDataLoader.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct ImageDataLoader { 11 | let load: (URL) -> AnyPublisher 12 | } 13 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/API/ImageDataWebLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDataWebLoader.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | final class ImageDataWebLoader { 11 | private let queue = DispatchQueue(label: "ImageWebAPI") 12 | private let client: HTTPClient 13 | init(client: HTTPClient) { 14 | self.client = client 15 | } 16 | 17 | var loader: ImageDataLoader { 18 | ImageDataLoader { [self] url in 19 | client.send(request: URLRequest(url: url)) 20 | .subscribe(on: queue) 21 | .eraseToAnyPublisher() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/API/URLSessionHTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | struct URLSessionHTTPClient: HTTPClient { 11 | private let session: URLSession 12 | init(session: URLSession) { 13 | self.session = session 14 | } 15 | 16 | func send(request: URLRequest) -> AnyPublisher { 17 | session.dataTaskPublisher(for: request) 18 | .tryMap { (data, response) in 19 | guard let httpResponse = response as? HTTPURLResponse, 20 | httpResponse.statusCode == 200 else { 21 | throw URLError(.badServerResponse) 22 | } 23 | return data 24 | } 25 | .eraseToAnyPublisher() 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | @UIApplicationMain 10 | class AppDelegate: UIResponder, UIApplicationDelegate { 11 | 12 | 13 | 14 | func application(_ application: UIApplication, 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, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 22 | // Called when a new scene session is being created. 23 | // Use this method to select a configuration to create the new scene with. 24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 25 | } 26 | 27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 28 | // Called when the user discards a scene session. 29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 31 | } 32 | 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/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 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/Models/Breed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Breed.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | typealias BreedType = String 10 | 11 | struct Breed: Equatable, Decodable, Hashable { 12 | let name: String 13 | let subBreeds: [Breed] 14 | 15 | var id: Self { self } 16 | } 17 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/Models/DogImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DogImage.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | struct DogImage: Equatable, Decodable, Hashable { 10 | let imageURL: URL 11 | } 12 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CombineCollection 4 | // 5 | // Created by Shinzan Takata on 2020/07/19. 6 | // 7 | 8 | import UIKit 9 | 10 | class BreedListViewController: UIViewController { 11 | struct Item: Hashable { 12 | let id: String 13 | } 14 | 15 | enum Section { 16 | case main 17 | } 18 | 19 | private var dataSource: UICollectionViewDiffableDataSource! 20 | private var collectionView: UICollectionView! 21 | private let appearance = UICollectionLayoutListConfiguration.Appearance.plain 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | navigationItem.title = "List" 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/Views/BreedImagesGrid/BreedImagesGridViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedImagesGridViewModel.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | final class BreedImagesGridViewModel { 11 | @Published private(set) var dogImages: [DogImage] = [] 12 | @Published private(set) var isLoading: Bool = false 13 | @Published private(set) var error: Error? = nil 14 | 15 | private let loader: DogImageListLoader 16 | init(loader: DogImageListLoader) { 17 | self.loader = loader 18 | } 19 | 20 | func fetch(breedType: BreedType) { 21 | isLoading = true 22 | error = nil 23 | 24 | loader.load(breedType) 25 | .replaceError(with: []) 26 | .receiveOnMainQueue() 27 | .handleEvents( 28 | receiveOutput: { [weak self] _ in 29 | self?.isLoading = false 30 | }, 31 | receiveCompletion: { [weak self] finished in 32 | self?.isLoading = false 33 | if case .failure(let error) = finished { 34 | self?.error = error 35 | } 36 | } 37 | ) 38 | .assign(to: &$dogImages) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/Views/BreedList/BreedListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreedListViewModel.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | final class BreedListViewModel { 11 | @Published private(set) var breeds: [DisplayBreed] = [] 12 | @Published private(set) var isLoading: Bool = false 13 | @Published private(set) var error: Error? = nil 14 | 15 | private let loader: BreedListLoader 16 | init(loader: BreedListLoader) { 17 | self.loader = loader 18 | } 19 | 20 | func fetchList() { 21 | isLoading = true 22 | error = nil 23 | loader.load() 24 | .map { $0.sorted(by: { $0.name < $1.name }) } 25 | .map(makeDisplayModels(from:)) 26 | .receiveOnMainQueue() 27 | .handleEvents( 28 | receiveOutput: { [weak self] _ in 29 | self?.isLoading = false 30 | }, 31 | receiveCompletion: { [weak self] finished in 32 | self?.isLoading = false 33 | if case .failure(let error) = finished { 34 | self?.error = error 35 | } 36 | } 37 | ) 38 | .replaceError(with: []) 39 | .assign(to: &$breeds) 40 | } 41 | 42 | func makeDisplayModels(from breeds: [Breed]) -> [DisplayBreed] { 43 | breeds.map(makeDisplayModel(from:)) 44 | } 45 | 46 | private func makeDisplayModel(from breed: Breed) -> DisplayBreed { 47 | let children = breed.subBreeds 48 | .map { DisplayBreed(name: "\(breed.name)/\($0.name)", displayName: $0.name.uppercased()) 49 | } 50 | let allKindsItem = DisplayBreed(name: breed.name, displayName: "\(breed.name.uppercased())の全種別") 51 | return DisplayBreed(name: breed.name, displayName: breed.name.uppercased(), 52 | subBreeds: [allKindsItem] + children) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/Views/BreedList/DisplayBreed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayBreed.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | struct DisplayBreed: Hashable { 10 | let name: String 11 | let displayName: String 12 | let subBreeds: [DisplayBreed] 13 | init(name: String, displayName: String, 14 | subBreeds: [DisplayBreed] = []) { 15 | self.name = name 16 | self.displayName = displayName 17 | self.subBreeds = subBreeds 18 | } 19 | } 20 | 21 | extension DisplayBreed: Identifiable { 22 | var id: Self { self } 23 | 24 | static var anyBreed: DisplayBreed { 25 | DisplayBreed(name: "child1", displayName: "child1") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollection/Views/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingView.swift 3 | // CombineCollection 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | final class LoadingView: UIView { 10 | private lazy var indicator: UIActivityIndicatorView = { 11 | let indicator = UIActivityIndicatorView(style: .large) 12 | indicator.hidesWhenStopped = true 13 | addSubview(indicator) 14 | indicator.translatesAutoresizingMaskIntoConstraints = false 15 | NSLayoutConstraint.activate([ 16 | indicator.centerXAnchor.constraint(equalTo: centerXAnchor), 17 | indicator.centerYAnchor.constraint(equalTo: centerYAnchor) 18 | ]) 19 | return indicator 20 | }() 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | setup() 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | func start() { 32 | indicator.startAnimating() 33 | } 34 | 35 | func stop() { 36 | indicator.stopAnimating() 37 | } 38 | 39 | private func setup() { 40 | backgroundColor = UIColor.lightGray.withAlphaComponent(0.5) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Schedulers/CombineCollection_RunOnMainScheduler/CombineCollectionTests/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 | -------------------------------------------------------------------------------- /Schedulers/Scheduler: -------------------------------------------------------------------------------- 1 | 7VrZcpswFP0azaQPyQAy26O3tJ20k87kIckjBhlIZURkEdv5+kogDBgvJLUNSes8WDraz73naokBHM6WX6kTBz+JhzDQFG8J4AhomqrYGv8SyCpDTM3KAJ+GnqxUAHfhK8pbSjQJPTSvVGSEYBbGVdAlUYRcVsEcSsmiWm1KcHXU2PFRDbhzHVxH70OPBRlqaWaBf0OhH+Qjq4adlcycvLJcyTxwPLIoQXAM4JASwrLUbDlEWJCX85K1u95Rup4YRRFr0uDx2RqG0FJvbu4nlj94uP/+9Hopl/Hi4EQumCIXhS/ogkQA9r/IqbNVzgclSeQh0aUC4GARhAzdxY4rShfcAzgWsBnmOZUnpyRi0qSaLfIhxkOCCeVARCKOD14QZSGnu49Dn484YkT0IWfFy9By53LVNYnc+xCZIUZXvIpsYErapd/pUOYXhRUNS2JB2YI56EjP8dddF+TyhOT3DVwbNa7nyWTu0nDy6dhek3iIbdg7Fdt2je27nG0KNAPzCQwmIuWzlIIMERxWTGA8JyQvuJyn7PZ5Bc7Nsijc7GUeO1GTXlRzWy+3CYsTdjEksxgjFpLoCxjrwLoGtpknjHwkTk02WHUC9YXxitnacvhUngY06OnI8nqCB0bJb1QqsbQJNIzjuJyubijcrPucam9TuHYqn1uH+oJX5PHtRGYJZQHxSeTgcYEOqswXdX4QIc6U7yfE2EoS7iSMVK2BliF7KKUfRVdXusyNlrLnNLOSmU0LZvMWk91vDb42klAX7SFB7u/MoT5ihwRaty5F2GF8A6ru5Me2lFWLDseJALcxog4jeyLMOYU41cXfNiEa6Uf2UMKzz3EEandOnypsU5/KlaaXJKruFSjnmK7KrUT+sVxYtEtzZ1B2fv4+pGzYprLVhvt+g93Z2LfH/12cWJ/8WtmfHWRN3a1hwbXQZHoS+W85f59b/b121W+W1a80Vb9ZUb/anvphQ/Xv8IozqV//KCewwsSV6H6ld9/CvTYtDGvx/VcyweE86GB4j8XtDYx7YDAC9hCMDWBZQAS9z3on24j5mt5+0Dc+SkA4obB7DYWttyns3kcQdvY28y9JGird07TV7kHuXbe4rlzi9KbHOGW7W5wnGOjnDAbvCxBverw1waCfJvrA3v0U1IU3o3Yeb7sQVexWo0o3Tgq5WQ6GB6vVW55SCw//n2+P/Xy7KVFon1CiPFv8JzwtK/2eAI7/AA== -------------------------------------------------------------------------------- /Schedulers/Scheduler.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Schedulers/Scheduler.md -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTest.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTest/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTest/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTest/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SchedulerTest 4 | // 5 | // 6 | 7 | import SwiftUI 8 | 9 | struct ContentView: View { 10 | var body: some View { 11 | Text("Hello, world!").padding() 12 | } 13 | } 14 | 15 | struct ContentView_Previews: PreviewProvider { 16 | static var previews: some View { 17 | ContentView() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTest/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 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTest/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTest/SchedulerTestApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchedulerTestApp.swift 3 | // SchedulerTest 4 | // 5 | // 6 | 7 | import SwiftUI 8 | 9 | @main 10 | struct SchedulerTestApp: App { 11 | var body: some Scene { 12 | WindowGroup { 13 | ContentView() 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTestTests/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // CombineCollectionTests 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | import XCTest 10 | 11 | struct Episode: Equatable { 12 | let id: Int 13 | } 14 | 15 | struct ApiClient { 16 | static let mock = ApiClient() 17 | 18 | func fetchEpisodes() -> AnyPublisher<[Episode], Never> { 19 | Deferred { 20 | Future { promise in 21 | promise(.success([Episode(id: 42)])) 22 | } 23 | } 24 | .eraseToAnyPublisher() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTestTests/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 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTestTests/SchedulerTests_AnyScheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchedulerTests_AnyScheduler.swift 3 | // SchedulerTestTests 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | import UIKit 10 | import XCTest 11 | 12 | private class HomeViewModel: ObservableObject { 13 | @Published private(set) var episodes: [Episode] = [] 14 | 15 | private let apiClient: ApiClient 16 | private let scheduler: AnySchedulerOf 17 | init(apiClient: ApiClient, scheduler: AnySchedulerOf) { 18 | self.apiClient = apiClient 19 | self.scheduler = scheduler 20 | } 21 | 22 | func reloadButtonTapped() { 23 | Just(()) 24 | .delay(for: .seconds(10), scheduler: scheduler) 25 | .flatMap { self.apiClient.fetchEpisodes() } 26 | .receive(on: scheduler) 27 | .assign(to: &$episodes) 28 | } 29 | } 30 | 31 | class SchedulerTests_AnyScheduler: XCTestCase { 32 | var cancellables = Set() 33 | 34 | func testViewModel() { 35 | let viewModel = HomeViewModel( 36 | apiClient: .mock, 37 | scheduler: DispatchQueue.immediateScheduler.eraseToAnyScheduler()) 38 | 39 | var output: [[Episode]] = [] 40 | viewModel.$episodes 41 | .sink { output.append($0) } 42 | .store(in: &self.cancellables) 43 | 44 | viewModel.reloadButtonTapped() 45 | XCTAssertEqual(output, [[], [Episode(id: 42)]]) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTestTests/SchedulerTests_ImmediateScheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchedulerTests_ImmediateScheduler.swift 3 | // SchedulerTestTests 4 | // 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | import UIKit 10 | import XCTest 11 | 12 | private class HomeViewModel: ObservableObject { 13 | @Published private(set) var episodes: [Episode] = [] 14 | 15 | private let apiClient: ApiClient 16 | private let scheduler: S 17 | init(apiClient: ApiClient, scheduler: S) { 18 | self.apiClient = apiClient 19 | self.scheduler = scheduler 20 | } 21 | 22 | func reloadButtonTapped() { 23 | Just(()) 24 | .delay(for: .seconds(10), scheduler: scheduler) 25 | .flatMap { self.apiClient.fetchEpisodes() } 26 | .receive(on: scheduler) 27 | .assign(to: &$episodes) 28 | } 29 | } 30 | 31 | class SchedulerTests_ImmediateScheduler: XCTestCase { 32 | var cancellables = Set() 33 | 34 | func testViewModel() { 35 | let viewModel = HomeViewModel( 36 | apiClient: .mock, 37 | scheduler: Combine.ImmediateScheduler.shared 38 | ) 39 | 40 | var output: [[Episode]] = [] 41 | viewModel.$episodes 42 | .sink { output.append($0) } 43 | .store(in: &self.cancellables) 44 | 45 | viewModel.reloadButtonTapped() 46 | XCTAssertEqual(output, [[], [Episode(id: 42)]]) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Schedulers/SchedulerTest/SchedulerTestTests/SchedulerTests_RunOnMainQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchedulerTests_RunOnMainQueue.swift 3 | // SchedulerTestTests 4 | // 5 | // 6 | 7 | import Combine 8 | import XCTest 9 | 10 | private class HomeViewModel: ObservableObject { 11 | @Published private(set) var episodes: [Episode] = [] 12 | 13 | private let apiClient: ApiClient 14 | init(apiClient: ApiClient) { 15 | self.apiClient = apiClient 16 | } 17 | 18 | func reloadButtonTapped() { 19 | Just(()) 20 | .delay(for: .seconds(10), scheduler: DispatchQueue.runOnMainQueueScheduler) 21 | .flatMap { self.apiClient.fetchEpisodes() } 22 | .receiveOnMainQueue() 23 | .assign(to: &$episodes) 24 | } 25 | } 26 | 27 | class SchedulerTests_RunOnMainQueue: XCTestCase { 28 | 29 | var cancellables = Set() 30 | 31 | func testViewModel() { 32 | let viewModel = HomeViewModel(apiClient: .mock) 33 | 34 | var output: [[Episode]] = [] 35 | viewModel.$episodes 36 | .sink { value in 37 | output.append(value) 38 | } 39 | .store(in: &self.cancellables) 40 | 41 | viewModel.reloadButtonTapped() 42 | _ = XCTWaiter().wait(for: [XCTestExpectation()], timeout: 11) 43 | XCTAssertEqual(output, [[], [Episode(id: 42)]]) 44 | } 45 | 46 | func testViewModel_reloadTwice() { 47 | let viewModel = HomeViewModel(apiClient: .mock) 48 | 49 | var output: [[Episode]] = [] 50 | viewModel.$episodes 51 | .sink { value in 52 | output.append(value) 53 | } 54 | .store(in: &self.cancellables) 55 | 56 | viewModel.reloadButtonTapped() 57 | viewModel.reloadButtonTapped() 58 | _ = XCTWaiter().wait(for: [XCTestExpectation()], timeout: 12) 59 | XCTAssertEqual(output, [[], [Episode(id: 42)], [Episode(id: 42)]]) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Schedulers/Schedulers.playground/Pages/receiveOn.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // Threadを切り替えているため 9 | // subscribe(on:)後の処理はMainThread以外で行われ 10 | // さらにreceive(on:)を呼ぶことで再びMainThreadで処理が行われている 11 | // receive(on:)以降は全てMainThreadで処理が行われている 12 | // Thread番号はDispatchQueueは最適なThreadを自動で選択するため動的に変わります 13 | run("receive(on:)") { 14 | let queue = DispatchQueue(label: "subscrbeQueue\(UUID().uuidString)") 15 | 16 | print("start: \(Thread.current.number)") 17 | 18 | [1,2,3,4].publisher 19 | .subscribe(on: queue) 20 | .handleEvents(receiveSubscription: { _ in print("before receive(on:) handleEvents receiveSubscription: \(Thread.current.number)") }, 21 | receiveOutput: { _ in print("before receive(on:) handleEvents receiveOutput: \(Thread.current.number)") }, 22 | receiveCompletion: { _ in print("before receive(on:) handleEvents receiveCompletion: \(Thread.current.number)") }, 23 | receiveCancel: { print("before receive(on:) handleEvents receiveCancel: \(Thread.current.number)") }, 24 | receiveRequest: { _ in print("before receive(on:) handleEvents receiveRequest: \(Thread.current.number)") }) 25 | .receive(on: DispatchQueue.main) 26 | .handleEvents(receiveOutput: { _ in print("after receive(on:) handleEvents receiveOutput: \(Thread.current.number)") }, 27 | receiveCompletion: { _ in print("after receive(on:)handleEvents receiveCompletion: \(Thread.current.number)") }) 28 | .sink { _ in print("sink receivedValue: \(Thread.current.number)") } 29 | .store(in: &cancellables) 30 | } 31 | 32 | //: [Next](@next) 33 | -------------------------------------------------------------------------------- /Schedulers/Schedulers.playground/Pages/subscribeOn.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // Threadを切り替えているため 9 | // subscribe(on:)後の処理はMainThread以外で行われている 10 | // Thread番号はDispatchQueueは最適なThreadを自動で選択するため動的に変わります 11 | run("subscribe(on:)") { 12 | let queue = DispatchQueue(label: "subscrbeQueue\(UUID().uuidString)") 13 | 14 | print("start: \(Thread.current.number)") 15 | 16 | [1,2,3,4].publisher 17 | .subscribe(on: queue) 18 | .handleEvents(receiveSubscription: { _ in print("handleEvents receiveSubscription: \(Thread.current.number)") }, 19 | receiveOutput: { _ in print("handleEvents receiveOutput: \(Thread.current.number)") }, 20 | receiveCompletion: { _ in print("handleEvents receiveCompletion: \(Thread.current.number)") }, 21 | receiveCancel: { print("handleEvents receiveCancel: \(Thread.current.number)") }, 22 | receiveRequest: { _ in print("handleEvents receiveRequest: \(Thread.current.number)") }) 23 | .sink { _ in print("sink receivedValue: \(Thread.current.number)") } 24 | .store(in: &cancellables) 25 | } 26 | 27 | //: [Next](@next) 28 | -------------------------------------------------------------------------------- /Schedulers/Schedulers.playground/Pages/subscribeOnNoSwitch.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | 6 | var cancellables = Set() 7 | 8 | // Threadの切り替えを行なっていないため 9 | // 全ての処理はMainThreadで行われている(Playgroundでは) 10 | run("subscribe(on:) Thread切り替えなし") { 11 | print("start: \(Thread.current.number)") 12 | 13 | [1,2,3,4].publisher 14 | .handleEvents(receiveSubscription: { _ in print("handleEvents receiveSubscription: \(Thread.current.number)") }, 15 | receiveOutput: { _ in print("handleEvents receiveOutput: \(Thread.current.number)") }, 16 | receiveCompletion: { _ in print("handleEvents receiveCompletion: \(Thread.current.number)") }, 17 | receiveCancel: { print("handleEvents receiveCancel: \(Thread.current.number)") }, 18 | receiveRequest: { _ in print("handleEvents receiveRequest: \(Thread.current.number)") }) 19 | .sink { _ in print("sink receivedValue: \(Thread.current.number)") } 20 | .store(in: &cancellables) 21 | } 22 | 23 | //: [Next](@next) 24 | -------------------------------------------------------------------------------- /Schedulers/Schedulers.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Timelane/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/1.png -------------------------------------------------------------------------------- /Timelane/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/10.png -------------------------------------------------------------------------------- /Timelane/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/11.png -------------------------------------------------------------------------------- /Timelane/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/12.png -------------------------------------------------------------------------------- /Timelane/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/13.png -------------------------------------------------------------------------------- /Timelane/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/14.png -------------------------------------------------------------------------------- /Timelane/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/2.png -------------------------------------------------------------------------------- /Timelane/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/3.png -------------------------------------------------------------------------------- /Timelane/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/4.png -------------------------------------------------------------------------------- /Timelane/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/5.png -------------------------------------------------------------------------------- /Timelane/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/6.png -------------------------------------------------------------------------------- /Timelane/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/7.png -------------------------------------------------------------------------------- /Timelane/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/8.png -------------------------------------------------------------------------------- /Timelane/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stzn/CombineStudy/1de9f1a8f34fe61e8f95438142ced2db080c6472/Timelane/9.png -------------------------------------------------------------------------------- /Timelane/Timelane.md: -------------------------------------------------------------------------------- 1 | #Timelane 2 | 3 | Xcode の Instruments を活用して 4 | Combine のデータの流れをヴィジュアライズするツールです。 5 | (RxSwift など他の Rx ライブラリにも利用できます。) 6 | 7 | https://github.com/icanzilb/Timelane 8 | 9 | Timelane について作者の方が解説している動画です。 10 | [Fixing your Combine code with the Timelane Instrument](https://www.youtube.com/watch?v=QfGZUfLw5AA) 11 | 12 | # セットアップ 13 | 14 | ※ 今回は Swift Package Manager を利用しますが 15 | Carthage や Cocoapods でも利用可能です 16 | 17 | ## Instruments テンプレートのインストール 18 | 19 | [インストールページ](https://github.com/icanzilb/Timelane/releases)より最新の app ファイルをダウンロードします。 20 | 21 | ダウンロードしたファイルを開いたページの 22 | ② に従ってテンプレートをダウンロードします。 23 | 24 | ![セットアップ1](1.png) 25 |
26 |
27 |
28 | 正常にインストールされると下記のようにメニューの中に出てきます。 29 | 30 | ![セットアップ2](3.png) 31 | 32 |
33 |
34 | 35 | ## プロジェクトへの設定 36 | 37 | ③ で Combine を選択すると 38 | プロジェクトへの設定方法が出てくるので 39 | これに従ってプロジェクトに追加します。 40 | 41 | ![セットアップ3](2.png) 42 | 43 | ### 1. Swift Package Manager を選択 44 | 45 | ![セットアップ4](4.png) 46 | 47 | ### 2. URL を入力して Next を押す 48 | 49 | ![セットアップ5](5.png) 50 | 51 | ### 3. Next を押す(設定はそのままで良いと思います) 52 | 53 | ![セットアップ6](6.png) 54 | 55 | ### 4. 正しいライブラリが設定されいることを確認し Finish を押す 56 | 57 | ![セットアップ7](7.png) 58 | 59 | ### 5. 正しくインストールされてれば下記のようにプロジェクト内に出てきます。 60 | 61 | ![セットアップ8](8.png) 62 | 63 | ![セットアップ9](9.png) 64 | 65 | ## 使用方法 66 | 67 | ### TimelaneCombine をインポートする 68 | 69 | ![セットアップ10](10.png) 70 | 71 | ### チェックしたい Publisher から lane メソッドを呼ぶ 72 | 73 | ![セットアップ11](11.png) 74 | 75 | ### Instruments を起して(Cmd + I)を Timelane を選択する 76 | 77 | ![セットアップ12](12.png) 78 | 79 | ### 左上のスタートボタンを押す 80 | 81 | ![セットアップ13](13.png) 82 | 83 | ### アプリの操作を行うと Publisher の出力結果が表示される 84 | 85 | 下記のケースは TextField に値を入力すると 86 | Publisher から Output が出力されています。 87 | 88 | ![セットアップ14](14.png) 89 | --------------------------------------------------------------------------------