├── .gitignore ├── 06-time-based-operators └── Playgrounds │ └── Chapter 6.playground │ ├── Pages │ ├── Challenge.xcplaygroundpage │ │ ├── Contents.swift │ │ └── Sources │ │ │ └── Utils.swift │ ├── Collecting Values.xcplaygroundpage │ │ └── Contents.swift │ ├── Holding Off On Events - Debonuce.xcplaygroundpage │ │ └── Contents.swift │ ├── Holding Off On Events - Throttle.xcplaygroundpage │ │ └── Contents.swift │ ├── Holding Off On Events - Timeout.xcplaygroundpage │ │ └── Contents.swift │ ├── Measuring Time.xcplaygroundpage │ │ └── Contents.swift │ └── Shifting Time.xcplaygroundpage │ │ └── Contents.swift │ ├── Sources │ ├── Data.swift │ ├── DeltaTime.swift │ ├── Model.swift │ └── Views.swift │ └── contents.xcplayground ├── 07-sequence-based-operators └── Playgrounds │ └── Chapter7.playground │ ├── Pages │ ├── Finding Values.xcplaygroundpage │ │ └── Contents.swift │ └── Querying Publishers.xcplaygroundpage │ │ └── Contents.swift │ ├── Sources │ └── Utils.swift │ └── contents.xcplayground ├── 08-photo-collage-app └── Collage │ ├── Collage.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ ├── Collage │ ├── AppDelegate.swift │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Data │ │ └── Models │ │ │ └── ImageCollage.swift │ ├── Info.plist │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── Resources │ │ └── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── image-1.imageset │ │ │ ├── Contents.json │ │ │ └── image-1.jpg │ │ │ ├── image-2.imageset │ │ │ ├── Contents.json │ │ │ └── image-2.jpg │ │ │ ├── image-3.imageset │ │ │ ├── Contents.json │ │ │ └── image-3.jpg │ │ │ └── image-4.imageset │ │ │ ├── Contents.json │ │ │ └── image-4.jpg │ ├── Reusables │ │ ├── CollageWriter.swift │ │ └── Extensions │ │ │ └── UIImage+Collage.swift │ ├── SceneDelegate.swift │ └── Scenes │ │ └── Collage │ │ ├── AddCollagePhotosView.swift │ │ ├── CollageGrid.swift │ │ ├── CurrentCollageContainerView.swift │ │ ├── CurrentCollageContainerViewModel.swift │ │ ├── CurrentCollageView.swift │ │ └── CurrentCollageViewModel.swift │ └── Resources │ ├── image-1.jpg │ ├── image-2.jpg │ ├── image-3.jpg │ └── image-4.jpg ├── 13-resource-management └── Playgrounds │ └── MyPlayground.playground │ ├── Pages │ ├── Multicasting.xcplaygroundpage │ │ └── Contents.swift │ ├── The Share Operator Pitfalls.xcplaygroundpage │ │ └── Contents.swift │ └── The Share Operator.xcplaygroundpage │ │ └── Contents.swift │ ├── Sources │ └── Utils.swift │ └── contents.xcplayground ├── 15-19-bitcoin-average-api-app └── BitcoinAverageAPIFetcher │ ├── BitcoinAverageAPIFetcher.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── BitcoinAverageAPIFetcher.xcscheme │ └── BitcoinAverageAPIFetcher │ ├── AppDelegate.swift │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── Data │ └── State │ │ ├── AppState.swift │ │ ├── PricesState.swift │ │ └── SettingsState.swift │ ├── Info.plist │ ├── Networking │ └── Dependencies.swift │ ├── Preview Content │ ├── Preview Assets.xcassets │ │ └── Contents.json │ └── Sample Data │ │ └── SampleData.swift │ ├── Resources │ └── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ └── Contents.json │ │ └── Contents.json │ ├── Reusables │ ├── Extensions │ │ └── BitcoinPrice+UpdatedAgoText.swift │ ├── Formatters │ │ ├── DateFormatters.swift │ │ └── NumberFormatters.swift │ └── Views │ │ ├── ShitcoinFilterListItem.swift │ │ ├── ShitcoinFilterSelectionView.swift │ │ └── TimestampBadge.swift │ ├── SceneDelegate.swift │ └── Scenes │ ├── Prices │ ├── PricesListContainerView.swift │ ├── PricesListView.swift │ └── PricesListViewModel.swift │ └── Settings │ ├── SettingsContainerView.swift │ ├── SettingsView.swift │ └── SettingsViewModel.swift ├── 16-error-handling └── Playgrounds │ └── MyPlayground.playground │ ├── Pages │ ├── AssertNoFailure.xcplaygroundpage │ │ └── Contents.swift │ ├── Assign.xcplaygroundpage │ │ └── Contents.swift │ ├── Catching and Retrying.xcplaygroundpage │ │ ├── Contents.swift │ │ └── Sources │ │ │ └── PhotoService.swift │ ├── Never.xcplaygroundpage │ │ └── Contents.swift │ └── try* Operators.xcplaygroundpage │ │ └── Contents.swift │ ├── Resources │ ├── hq.jpg │ ├── lq.jpg │ └── na.jpg │ ├── Sources │ └── Utils.swift │ └── contents.xcplayground ├── 17-schedulers └── Playgrounds │ └── MyPlayground.playground │ ├── Pages │ ├── Challenge 1 - Stop the Timer.xcplaygroundpage │ │ └── Contents.swift │ ├── Challenge 2 - Discover Optimization.xcplaygroundpage │ │ └── Contents.swift │ ├── DispatchQueue.xcplaygroundpage │ │ └── Contents.swift │ ├── ImmediateScheduler.xcplaygroundpage │ │ └── Contents.swift │ ├── OperationQueue.xcplaygroundpage │ │ └── Contents.swift │ ├── RunLoop.xcplaygroundpage │ │ └── Contents.swift │ └── subscribeOn & receieveOn.xcplaygroundpage │ │ └── Contents.swift │ ├── Sources │ ├── Computation.swift │ ├── DeltaTime.swift │ ├── Model.swift │ ├── PlaygroundUtils.swift │ ├── Record.swift │ ├── Thread.swift │ └── Views.swift │ └── contents.xcplayground ├── 18-custom-publishers-and-handling-backpressure └── Playgrounds │ └── MyPlayground.playground │ ├── Pages │ ├── DispatchTimer Publisher.xcplaygroundpage │ │ └── Contents.swift │ ├── PausableSink.xcplaygroundpage │ │ └── Contents.swift │ ├── ShareReplay Operator.xcplaygroundpage │ │ └── Contents.swift │ └── Unwrap Operator.xcplaygroundpage │ │ └── Contents.swift │ ├── Sources │ ├── PlaygroundUtils.swift │ └── TimeLogger.swift │ └── contents.xcplayground ├── 19-testing-combine-code └── Projects │ └── ColorCalc │ ├── ColorCalc.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ ├── ColorCalc │ ├── App │ │ ├── AppDelegate.swift │ │ └── SceneDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Extensions │ │ └── Color+.swift │ ├── Info.plist │ ├── Models │ │ └── ColorName.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── View Models │ │ └── CalculatorViewModel.swift │ └── Views │ │ ├── CalculatorButton.swift │ │ ├── CalculatorView.swift │ │ └── DisplayView.swift │ └── ColorCalcTests │ ├── Combine Operators │ ├── CombineOperatorsTests+Collect.swift │ ├── CombineOperatorsTests+FlatMap.swift │ ├── CombineOperatorsTests+ShareReplay.swift │ └── CombineOperatorsTests+TimerPublisher.swift │ ├── CalculatorViewModelTests.swift │ ├── ColorCalcTests.swift │ ├── CombineOperatorsTests.swift │ └── Info.plist ├── 20-building-a-complete-app ├── BookExample │ ├── challenge │ │ ├── final │ │ │ ├── ChuckNorrisJokes.xcodeproj │ │ │ │ ├── project.pbxproj │ │ │ │ ├── project.xcworkspace │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ └── xcshareddata │ │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ │ └── xcshareddata │ │ │ │ │ └── xcschemes │ │ │ │ │ └── ChuckNorrisJokes.xcscheme │ │ │ ├── ChuckNorrisJokes │ │ │ │ ├── App │ │ │ │ │ ├── AppDelegate.swift │ │ │ │ │ └── SceneDelegate.swift │ │ │ │ ├── Assets.xcassets │ │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ │ │ └── ItunesArtwork@2x.png │ │ │ │ │ ├── Colors │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── Gray.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── Green.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Red.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Base.lproj │ │ │ │ │ └── LaunchScreen.storyboard │ │ │ │ ├── ChuckNorrisJokesStyleKit.swift │ │ │ │ ├── Info.plist │ │ │ │ ├── Models │ │ │ │ │ ├── ChuckNorrisJokes.xcdatamodeld │ │ │ │ │ │ └── ChuckNorrisJokes.xcdatamodel │ │ │ │ │ │ │ └── contents │ │ │ │ │ └── JokeManagedObject+.swift │ │ │ │ ├── Preview Content │ │ │ │ │ └── Preview Assets.xcassets │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── SampleJoke.json │ │ │ │ └── Views │ │ │ │ │ ├── HUDView.swift │ │ │ │ │ ├── JokeCardView.swift │ │ │ │ │ ├── JokeView.swift │ │ │ │ │ ├── LargeInlineButton.swift │ │ │ │ │ └── SavedJokesView.swift │ │ │ ├── ChuckNorrisJokesModel │ │ │ │ ├── ChuckNorrisJokesModel.h │ │ │ │ ├── Extensions │ │ │ │ │ └── URLComponents+.swift │ │ │ │ ├── Info.plist │ │ │ │ ├── Models │ │ │ │ │ ├── Joke.swift │ │ │ │ │ └── TranslationResponse.swift │ │ │ │ ├── Protocols │ │ │ │ │ ├── JokeServiceDataPublisher.swift │ │ │ │ │ └── TranslationServiceDataPublisher.swift │ │ │ │ ├── Services │ │ │ │ │ ├── JokesService.swift │ │ │ │ │ └── TranslationService.swift │ │ │ │ └── View Models │ │ │ │ │ └── JokesViewModel.swift │ │ │ └── ChuckNorrisJokesTests │ │ │ │ ├── Info.plist │ │ │ │ ├── Services │ │ │ │ ├── MockJokesService.swift │ │ │ │ └── MockTranslationService.swift │ │ │ │ ├── TestJoke.json │ │ │ │ ├── TestTranslatedJoke.json │ │ │ │ ├── TestTranslationResponse.json │ │ │ │ └── Tests │ │ │ │ └── JokesViewModelTests.swift │ │ └── starter │ │ │ ├── ChuckNorrisJokes.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── ChuckNorrisJokes.xcscheme │ │ │ ├── ChuckNorrisJokes │ │ │ ├── App │ │ │ │ ├── AppDelegate.swift │ │ │ │ └── SceneDelegate.swift │ │ │ ├── Assets.xcassets │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ │ └── ItunesArtwork@2x.png │ │ │ │ ├── Colors │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Gray.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Green.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Red.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj │ │ │ │ └── LaunchScreen.storyboard │ │ │ ├── ChuckNorrisJokesStyleKit.swift │ │ │ ├── Info.plist │ │ │ ├── Models │ │ │ │ ├── ChuckNorrisJokes.xcdatamodeld │ │ │ │ │ └── ChuckNorrisJokes.xcdatamodel │ │ │ │ │ │ └── contents │ │ │ │ └── JokeManagedObject+.swift │ │ │ ├── Preview Content │ │ │ │ └── Preview Assets.xcassets │ │ │ │ │ └── Contents.json │ │ │ ├── SampleJoke.json │ │ │ └── Views │ │ │ │ ├── HUDView.swift │ │ │ │ ├── JokeCardView.swift │ │ │ │ ├── JokeView.swift │ │ │ │ ├── LargeInlineButton.swift │ │ │ │ └── SavedJokesView.swift │ │ │ ├── ChuckNorrisJokesModel │ │ │ ├── ChuckNorrisJokesModel.h │ │ │ ├── Extensions │ │ │ │ └── URLComponents+.swift │ │ │ ├── Info.plist │ │ │ ├── Models │ │ │ │ ├── Joke.swift │ │ │ │ └── TranslationResponse.swift │ │ │ ├── Protocols │ │ │ │ ├── JokeServiceDataPublisher.swift │ │ │ │ └── TranslationServiceDataPublisher.swift │ │ │ ├── Services │ │ │ │ ├── JokesService.swift │ │ │ │ └── TranslationService.swift │ │ │ └── View Models │ │ │ │ └── JokesViewModel.swift │ │ │ └── ChuckNorrisJokesTests │ │ │ ├── Info.plist │ │ │ ├── Services │ │ │ ├── MockJokesService.swift │ │ │ └── MockTranslationService.swift │ │ │ ├── TestJoke.json │ │ │ ├── TestTranslatedJoke.json │ │ │ ├── TestTranslationResponse.json │ │ │ └── Tests │ │ │ └── JokesViewModelTests.swift │ ├── final │ │ ├── ChuckNorrisJokes.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── ChuckNorrisJokes.xcscheme │ │ ├── ChuckNorrisJokes │ │ │ ├── App │ │ │ │ ├── AppDelegate.swift │ │ │ │ └── SceneDelegate.swift │ │ │ ├── Assets.xcassets │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ │ └── ItunesArtwork@2x.png │ │ │ │ ├── Colors │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Gray.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Green.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Red.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj │ │ │ │ └── LaunchScreen.storyboard │ │ │ ├── ChuckNorrisJokesStyleKit.swift │ │ │ ├── Info.plist │ │ │ ├── Models │ │ │ │ ├── ChuckNorrisJokes.xcdatamodeld │ │ │ │ │ └── ChuckNorrisJokes.xcdatamodel │ │ │ │ │ │ └── contents │ │ │ │ └── JokeManagedObject+.swift │ │ │ ├── Preview Content │ │ │ │ └── Preview Assets.xcassets │ │ │ │ │ └── Contents.json │ │ │ ├── SampleJoke.json │ │ │ └── Views │ │ │ │ ├── HUDView.swift │ │ │ │ ├── JokeCardView.swift │ │ │ │ ├── JokeView.swift │ │ │ │ ├── LargeInlineButton.swift │ │ │ │ └── SavedJokesView.swift │ │ ├── ChuckNorrisJokesModel │ │ │ ├── ChuckNorrisJokesModel.h │ │ │ ├── Extensions │ │ │ │ └── URLComponents+.swift │ │ │ ├── Info.plist │ │ │ ├── Models │ │ │ │ ├── Joke.swift │ │ │ │ └── TranslationResponse.swift │ │ │ ├── Protocols │ │ │ │ ├── JokeServiceDataPublisher.swift │ │ │ │ └── TranslationServiceDataPublisher.swift │ │ │ ├── Services │ │ │ │ ├── JokesService.swift │ │ │ │ └── TranslationService.swift │ │ │ └── View Models │ │ │ │ └── JokesViewModel.swift │ │ └── ChuckNorrisJokesTests │ │ │ ├── Info.plist │ │ │ ├── Services │ │ │ ├── MockJokesService.swift │ │ │ └── MockTranslationService.swift │ │ │ ├── TestJoke.json │ │ │ ├── TestTranslatedJoke.json │ │ │ ├── TestTranslationResponse.json │ │ │ └── Tests │ │ │ └── JokesViewModelTests.swift │ └── starter │ │ ├── ChuckNorrisJokes.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── ChuckNorrisJokes.xcscheme │ │ ├── ChuckNorrisJokes │ │ ├── App │ │ │ ├── AppDelegate.swift │ │ │ └── SceneDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── ItunesArtwork@2x.png │ │ │ ├── Colors │ │ │ │ ├── Contents.json │ │ │ │ ├── Gray.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── Green.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── Red.colorset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── ChuckNorrisJokesStyleKit.swift │ │ ├── Info.plist │ │ ├── Models │ │ │ └── ChuckNorrisJokes.xcdatamodeld │ │ │ │ └── ChuckNorrisJokes.xcdatamodel │ │ │ │ └── contents │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── SampleJoke.json │ │ └── Views │ │ │ ├── HUDView.swift │ │ │ ├── JokeCardView.swift │ │ │ ├── JokeView.swift │ │ │ ├── LargeInlineButton.swift │ │ │ └── SavedJokesView.swift │ │ ├── ChuckNorrisJokesModel │ │ ├── ChuckNorrisJokesModel.h │ │ ├── Extensions │ │ │ └── URLComponents+.swift │ │ ├── Info.plist │ │ ├── Models │ │ │ ├── Joke.swift │ │ │ └── TranslationResponse.swift │ │ ├── Protocols │ │ │ ├── JokeServiceDataPublisher.swift │ │ │ └── TranslationServiceDataPublisher.swift │ │ ├── Services │ │ │ ├── JokesService.swift │ │ │ └── TranslationService.swift │ │ └── View Models │ │ │ └── JokesViewModel.swift │ │ └── ChuckNorrisJokesTests │ │ ├── Info.plist │ │ ├── Services │ │ ├── MockJokesService.swift │ │ └── MockTranslationService.swift │ │ ├── TestJoke.json │ │ ├── TestTranslatedJoke.json │ │ ├── TestTranslationResponse.json │ │ └── Tests │ │ └── JokesViewModelTests.swift └── NumberFacts │ ├── Common │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── contents.xcworkspacedata │ ├── Package.swift │ ├── README.md │ └── Sources │ │ ├── Models │ │ ├── Language │ │ │ └── Language.swift │ │ └── NumberFact │ │ │ ├── NumberFact+Category.swift │ │ │ ├── NumberFact+CoreDataClass.swift │ │ │ ├── NumberFact+CoreDataProperties.swift │ │ │ ├── NumberFact+Decoder.swift │ │ │ └── NumberFact+FetchHelpers.swift │ │ └── SupportedLanguages.swift │ ├── Design │ └── screenshot-1.png │ ├── NumberFacts.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ ├── NumberFacts │ ├── NumberFacts.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── NumberFacts.xcscheme │ ├── NumberFacts │ │ ├── App │ │ │ ├── AppDelegate.swift │ │ │ ├── CurrentApplication.swift │ │ │ └── SceneDelegate.swift │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── Data │ │ │ ├── CoreDataManager+Utils.swift │ │ │ ├── NumberFacts.xcdatamodeld │ │ │ │ ├── .xccurrentversion │ │ │ │ └── NumberFacts.xcdatamodel │ │ │ │ │ └── contents │ │ │ └── State │ │ │ │ ├── AppState.swift │ │ │ │ └── NumberFactsState.swift │ │ ├── Info.plist │ │ ├── Preview Content │ │ │ ├── Preivew Data │ │ │ │ ├── PreviewData+AppStores.swift │ │ │ │ ├── PreviewData+NumberFacts.swift │ │ │ │ └── PreviewData.swift │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── Resources │ │ │ └── Assets.xcassets │ │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ ├── Reusables │ │ │ └── Extensions │ │ │ │ ├── Collection+deleteManagedObjects.swift │ │ │ │ └── NumberFact+errorFact.swift │ │ └── Scenes │ │ │ ├── Favorite Number Facts │ │ │ ├── FavoriteNumberFactsContainerView+ViewModel.swift │ │ │ ├── FavoriteNumberFactsContainerView.swift │ │ │ └── FavoriteNumberFactsListView.swift │ │ │ ├── HomeView.swift │ │ │ ├── Number Facts Feed │ │ │ ├── NumberFactCardView+ViewModel.swift │ │ │ ├── NumberFactCardView.swift │ │ │ ├── NumberFactsFeedContainerView+ViewModel.swift │ │ │ └── NumberFactsFeedContainerView.swift │ │ │ └── RootView.swift │ └── NumberFactsTests │ │ ├── Info.plist │ │ └── NumberFactsTests.swift │ ├── NumbersAPIService │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ ├── NumbersAPIService.xcscheme │ │ │ └── NumbersAPIServiceTests.xcscheme │ ├── Package.resolved │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ ├── Extensions │ │ │ └── Endpoint+NumbersAPI.swift │ │ ├── NumbersAPIService.swift │ │ ├── NumbersAPIServiceError.swift │ │ └── Protocols │ │ │ └── NumbersAPIServicing.swift │ └── Tests │ │ ├── LinuxMain.swift │ │ └── NumbersAPIService │ │ ├── NumbersAPIServiceTests.swift │ │ └── XCTestManifests.swift │ ├── Playgrounds │ └── NumbersAPI.playground │ │ ├── Contents.swift │ │ └── contents.xcplayground │ └── TranslationService │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ ├── Sources │ ├── Extensions │ │ └── Endpoint+TranslationAPI.swift │ ├── Protocols │ │ └── TranslationAPIServicing.swift │ ├── TranslationAPIService.swift │ └── TranslationAPIServiceError.swift │ └── Tests │ └── TranslationService │ └── TranslationServiceTests.swift ├── Playgrounds.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── README.md └── chapters-2-5 └── Playgrounds └── Chapters 2-5.playground ├── Pages ├── 02 - Challenge.xcplaygroundpage │ ├── Contents.swift │ └── Sources │ │ ├── Deck.swift │ │ ├── Hand.swift │ │ ├── HandError.swift │ │ ├── PlayingCard+Points.swift │ │ ├── PlayingCard.swift │ │ ├── Rank.swift │ │ └── Suit.swift ├── 02 - Dynamically Adjusting Demand.xcplaygroundpage │ ├── Contents.swift │ └── Sources │ │ └── IntSubscriber.swift ├── 02 - Publishers & Subscribers Intro.xcplaygroundpage │ ├── Contents.swift │ └── Sources │ │ └── IntSubscriber.swift ├── 02 - Subjects.xcplaygroundpage │ ├── Contents.swift │ └── Sources │ │ ├── CustomError.swift │ │ └── StringSubscriber.swift ├── 02 - Type Erasure.xcplaygroundpage │ └── Contents.swift ├── 03 - Challenge.xcplaygroundpage │ ├── Contents.swift │ └── Sources │ │ ├── Contacts.swift │ │ └── Phone.swift ├── 03 - Collecting Values.xcplaygroundpage │ └── Contents.swift ├── 03 - Flattening Publishers.xcplaygroundpage │ ├── Contents.swift │ └── Sources │ │ └── Chatter.swift ├── 03 - Incrementally Transforming Output.xcplaygroundpage │ └── Contents.swift ├── 03 - Mapping Keypaths.xcplaygroundpage │ ├── Contents.swift │ └── Sources │ │ └── Coordinate.swift ├── 03 - Mapping Values.xcplaygroundpage │ └── Contents.swift ├── 03 - Replacing Upstream Output.xcplaygroundpage │ └── Contents.swift ├── 03 - TryMap.xcplaygroundpage │ └── Contents.swift ├── 04 - Compacting and Ignoring.xcplaygroundpage │ └── Contents.swift ├── 04 - Dropping Values.xcplaygroundpage │ └── Contents.swift ├── 04 - Filtering Operators Intro.xcplaygroundpage │ └── Contents.swift ├── 04 - Finding Values.xcplaygroundpage │ └── Contents.swift ├── 04 - Limiting Values.xcplaygroundpage │ └── Contents.swift ├── 04- Challenge.xcplaygroundpage │ └── Contents.swift ├── 05 - Merge.xcplaygroundpage │ └── Contents.swift ├── 05 - Prepending.xcplaygroundpage │ └── Contents.swift ├── 05 - switchToLatest.xcplaygroundpage │ └── Contents.swift ├── 05 - zip.xcplaygroundpage │ └── Contents.swift ├── 05- Appending.xcplaygroundpage │ └── Contents.swift └── 05- combineLatest.xcplaygroundpage │ └── Contents.swift ├── Sources └── PlaygroundUtils.swift └── contents.xcplayground /06-time-based-operators/Playgrounds/Chapter 6.playground/Pages/Challenge.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | //: Challenge: 7 | //: 8 | //: Starting with: 9 | //: 10 | //: - A subject that emits integers. 11 | //: - A function call that feeds the subject with mysterious data. 12 | //: 13 | //: Your challenge is to: 14 | //: 15 | //: - Group data by batches of 0.5 seconds. 16 | //: - Turn the grouped data into a string. 17 | //: - If there is a pause longer than 0.9 seconds in the feed, print the 👏 emoji. 18 | //: - Print it. 19 | //: 20 | //: 21 | //: Hint: Create a second publisher for this step and merge it 22 | //: with the first publisher in your subscription. 23 | //: 24 | //: Note: To convert an Int to a Character, you can do something like Character(Unicode.Scalar(value)!). 25 | //: If you code this challenge correctly, you’ll see a sentence printed in the Debug area. What is it? 26 | 27 | 28 | 29 | let intStream = PassthroughSubject() 30 | let collectionInterval: TimeInterval = 0.5 31 | 32 | var subscriptions = Set() 33 | 34 | 35 | let strings = intStream 36 | .collect(.byTime( 37 | DispatchQueue.main, 38 | .seconds(collectionInterval) 39 | )) 40 | .map({ numbers in 41 | String(numbers.map { Character(Unicode.Scalar($0)!) }) 42 | }) 43 | 44 | 45 | 46 | let measurements = intStream 47 | .measureInterval(using: DispatchQueue.main) 48 | .compactMap({ stride in 49 | stride > 0.9 ? "👏" : nil 50 | }) 51 | 52 | 53 | Publishers.Merge(strings, measurements) 54 | .sink { value in 55 | print(value) 56 | } 57 | .store(in: &subscriptions) 58 | 59 | 60 | 61 | feedValues(to: intStream) 62 | 63 | 64 | 65 | //: [Next](@next) 66 | -------------------------------------------------------------------------------- /06-time-based-operators/Playgrounds/Chapter 6.playground/Pages/Challenge.xcplaygroundpage/Sources/Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | 5 | let samples: [(TimeInterval, Int)] = [ 6 | (0.05, 67), 7 | (0.10, 111), 8 | (0.15, 109), 9 | (0.20, 98), 10 | (0.25, 105), 11 | (0.30, 110), 12 | (0.35, 101), 13 | (1.50, 105), 14 | (1.55, 115), 15 | (2.60, 99), 16 | (2.65, 111), 17 | (2.70, 111), 18 | (2.75, 108), 19 | (2.80, 33), 20 | ] 21 | 22 | 23 | public func feedValues(to subject: S) where S.Output == Int { 24 | var lastDelay: TimeInterval = 0 25 | 26 | for (delay, number) in samples { 27 | lastDelay = delay 28 | 29 | DispatchQueue.main.asyncAfter(deadline: .now() + lastDelay) { 30 | subject.send(number) 31 | } 32 | } 33 | 34 | DispatchQueue.main.asyncAfter(deadline: .now() + lastDelay + 0.5) { 35 | subject.send(completion: .finished) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /06-time-based-operators/Playgrounds/Chapter 6.playground/Pages/Holding Off On Events - Debonuce.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import SwiftUI 5 | import PlaygroundSupport 6 | 7 | 8 | let debounceTime: TimeInterval = 1.0 9 | 10 | 11 | 12 | // MARK: - Publishers 13 | 14 | let basePublisher = PassthroughSubject() 15 | 16 | 17 | let debouncedPublisher = basePublisher 18 | .debounce(for: .seconds(debounceTime), scheduler: DispatchQueue.main) 19 | // Use `share()` to create a single subscription point to `debounce` 20 | // that will show the same results at the same time to all subscribers 21 | .share() 22 | 23 | 24 | 25 | // MARK: - Subscribers 26 | 27 | let subscription1 = basePublisher 28 | .sink { string in 29 | print("+\(deltaTime)s -- Subject emitted: \(string)") 30 | } 31 | 32 | 33 | let subscription2 = debouncedPublisher 34 | .sink { string in 35 | print("+\(deltaTime)s -- Subject emitted: \(string)") 36 | } 37 | 38 | 39 | 40 | // MARK: - Timelines 41 | 42 | let baseTimeline = TimelineView( 43 | title: "Emitted Values", 44 | events: [] 45 | ) 46 | 47 | 48 | let debounceTimeline = TimelineView( 49 | title: "Debounced Values (debounce time: \(debounceTime) seconds)", 50 | events: [] 51 | ) 52 | 53 | 54 | 55 | // MARK: - View Setup 56 | 57 | let view = VStack(spacing: 50) { 58 | baseTimeline 59 | debounceTimeline 60 | } 61 | 62 | 63 | PlaygroundPage.current.liveView = UIHostingController(rootView: view) 64 | 65 | 66 | basePublisher.displayEvents(in: baseTimeline) 67 | debouncedPublisher.displayEvents(in: debounceTimeline) 68 | 69 | 70 | 71 | // 💥 72 | 73 | basePublisher.feed(with: typingHelloWorld) 74 | 75 | 76 | //: [Next](@next) 77 | -------------------------------------------------------------------------------- /06-time-based-operators/Playgrounds/Chapter 6.playground/Pages/Holding Off On Events - Timeout.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import SwiftUI 5 | import PlaygroundSupport 6 | 7 | 8 | let timeoutLimit: TimeInterval = 5.0 9 | 10 | enum TimeoutError: Error { 11 | case timedOut 12 | } 13 | 14 | // MARK: - Publishers 15 | 16 | let basePublisher = PassthroughSubject() 17 | 18 | 19 | // The timedOut subject publisher will time out after X seconds without the 20 | // upstream publisher emitting any value 21 | let timeoutPublisher = basePublisher 22 | .timeout( 23 | .seconds(timeoutLimit), 24 | scheduler: RunLoop.main, 25 | customError: { .timedOut } 26 | ) 27 | 28 | 29 | 30 | // MARK: - Timelines 31 | 32 | let baseTimeline = TimelineView( 33 | title: "Button Taps", 34 | events: [] 35 | ) 36 | 37 | 38 | 39 | 40 | // MARK: - View Setup 41 | 42 | let view = VStack(spacing: 50) { 43 | baseTimeline 44 | 45 | Button(action: { 46 | // Send a signal to the base publisher -- which, in turn, will 47 | // be seen by the `timeoutPublisher`, which is a timeout-modfied version 48 | // of that publisher wired to stop if a signal isn't sent to the `basePublisher` within 49 | // the timeout period. 50 | basePublisher.send() 51 | }) { 52 | Text("Tap be within \(timeoutLimit) seconds") 53 | } 54 | } 55 | 56 | 57 | PlaygroundPage.current.liveView = UIHostingController(rootView: view) 58 | 59 | 60 | timeoutPublisher.displayEvents(in: baseTimeline) 61 | 62 | 63 | //: [Next](@next) 64 | -------------------------------------------------------------------------------- /06-time-based-operators/Playgrounds/Chapter 6.playground/Pages/Shifting Time.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import SwiftUI 5 | import PlaygroundSupport 6 | 7 | //: Creates a publisher that emits one value every second, 8 | //: then delays it by 1.5 seconds and displays both timelines simultaneously 9 | //: to compare them. 10 | 11 | 12 | let valuesPerSecond = 1.0 13 | let delayInSeconds = 1.5 14 | 15 | let basePublisher = PassthroughSubject() 16 | 17 | let delayedPublisher = basePublisher 18 | .delay(for: .seconds(delayInSeconds), scheduler: DispatchQueue.main) 19 | 20 | 21 | let subscriber = Timer 22 | .publish(every: 1.0 / valuesPerSecond, on: .main, in: .common) 23 | .autoconnect() 24 | .subscribe(basePublisher) 25 | 26 | 27 | let baseTimeline = TimelineView( 28 | title: "Emitted Values (\(valuesPerSecond) per second)", 29 | events: [] 30 | ) 31 | 32 | 33 | let delayedTimeline = TimelineView( 34 | title: "Delayed Values (delayed by \(delayInSeconds) seconds)", 35 | events: [] 36 | ) 37 | 38 | 39 | let view = VStack(spacing: 50) { 40 | baseTimeline 41 | delayedTimeline 42 | } 43 | 44 | 45 | PlaygroundPage.current.liveView = UIHostingController(rootView: view) 46 | 47 | 48 | basePublisher.displayEvents(in: baseTimeline) 49 | delayedPublisher.displayEvents(in: delayedTimeline) 50 | 51 | 52 | //: [Next](@next) 53 | 54 | -------------------------------------------------------------------------------- /06-time-based-operators/Playgrounds/Chapter 6.playground/Sources/Data.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | 5 | /// Sample data we use to feed a subject, simulating a user typing "Hello World" 6 | public let typingHelloWorld: [(TimeInterval, String)] = [ 7 | (0.0, "H"), 8 | (0.1, "He"), 9 | (0.2, "Hel"), 10 | (0.3, "Hell"), 11 | (0.5, "Hello"), 12 | (0.6, "Hello "), 13 | (2.0, "Hello W"), 14 | (2.1, "Hello Wo"), 15 | (2.2, "Hello Wor"), 16 | (2.4, "Hello Worl"), 17 | (2.5, "Hello World") 18 | ] 19 | 20 | 21 | public extension Subject where Output == String { 22 | 23 | /// A function that can feed delayed values to a subject for testing and simulation purposes 24 | func feed(with data: [(TimeInterval, String)]) { 25 | var lastDelay: TimeInterval = 0 26 | 27 | for (delay, text) in data { 28 | lastDelay = delay 29 | 30 | DispatchQueue.main.asyncAfter(deadline: .now() + lastDelay) { [unowned self] in 31 | self.send(text) 32 | } 33 | } 34 | 35 | DispatchQueue.main.asyncAfter(deadline: .now() + lastDelay + 1.5) { [unowned self] in 36 | self.send(completion: .finished) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /06-time-based-operators/Playgrounds/Chapter 6.playground/Sources/DeltaTime.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | let start = Date() 5 | 6 | 7 | let deltaFormatter: NumberFormatter = { 8 | let formatter = NumberFormatter() 9 | 10 | formatter.negativePrefix = "" 11 | formatter.minimumFractionDigits = 1 12 | formatter.maximumFractionDigits = 1 13 | 14 | return formatter 15 | }() 16 | 17 | 18 | 19 | /// A simple delta time formatter suitable for use in playground pages: start date is initialized every time the page starts running 20 | public var deltaTime: String { 21 | return deltaFormatter.string(for: Date().timeIntervalSince(start))! 22 | } 23 | -------------------------------------------------------------------------------- /06-time-based-operators/Playgrounds/Chapter 6.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /07-sequence-based-operators/Playgrounds/Chapter7.playground/Sources/Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public func demo(describing description: String, action: () -> Void) { 5 | print("\n--- Example of: \(description) ---") 6 | action() 7 | } 8 | -------------------------------------------------------------------------------- /07-sequence-based-operators/Playgrounds/Chapter7.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CypherPoetSwiftUIKit", 6 | "repositoryURL": "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "8b2d7eb75433ed4b8ad8bc9ff2a43a06dcf4bbe2", 10 | "version": "0.0.18" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Collage 4 | // 5 | // Created by Brian Sipple on 10/28/19. 6 | // Copyright © 2019 CypherPoet. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/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 | -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Data/Models/ImageCollage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCollage.swift 3 | // Collage 4 | // 5 | // Created by CypherPoet on 10/28/19. 6 | // ✌️ 7 | // 8 | 9 | import SwiftUI 10 | 11 | 12 | struct ImageCollage { 13 | let id = UUID() 14 | 15 | var name: String 16 | var processedImage: UIImage? 17 | } 18 | 19 | 20 | 21 | extension ImageCollage: Identifiable {} 22 | 23 | extension ImageCollage: Hashable { 24 | func hash(into hasher: inout Hasher) { 25 | hasher.combine(name) 26 | } 27 | } 28 | 29 | 30 | #if DEBUG 31 | 32 | let sampleCollage = ImageCollage( 33 | name: "Mass Effect", 34 | processedImage: nil 35 | ) 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/image-1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image-1.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/image-1.imageset/image-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/image-1.imageset/image-1.jpg -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/image-2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image-2.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/image-2.imageset/image-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/image-2.imageset/image-2.jpg -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/image-3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image-3.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/image-3.imageset/image-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/image-3.imageset/image-3.jpg -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/image-4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "image-4.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/image-4.imageset/image-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/08-photo-collage-app/Collage/Collage/Resources/Assets.xcassets/image-4.imageset/image-4.jpg -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Scenes/Collage/AddCollagePhotosView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddCollagePhotosView.swift 3 | // Collage 4 | // 5 | // Created by CypherPoet on 10/28/19. 6 | // ✌️ 7 | // 8 | 9 | import SwiftUI 10 | 11 | 12 | struct AddCollagePhotosView: View { 13 | } 14 | 15 | 16 | // MARK: - Body 17 | extension AddCollagePhotosView { 18 | 19 | var body: some View { 20 | Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/) 21 | } 22 | } 23 | 24 | 25 | // MARK: - Preview 26 | struct AddCollagePhotos_Previews: PreviewProvider { 27 | 28 | static var previews: some View { 29 | AddCollagePhotosView() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Collage/Scenes/Collage/CurrentCollageContainerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurrentCollageContainerViewModel.swift 3 | // Collage 4 | // 5 | // Created by CypherPoet on 11/3/19. 6 | // ✌️ 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | 13 | final class CurrentCollageContainerViewModel: ObservableObject { 14 | private var subscriptions = Set() 15 | 16 | 17 | @Published var isPhotoWriterAuthorized: Bool = false 18 | 19 | 20 | init() { 21 | setupSubscribers() 22 | } 23 | } 24 | 25 | 26 | 27 | // MARK: - Publishers 28 | extension CurrentCollageContainerViewModel { 29 | } 30 | 31 | 32 | 33 | 34 | // MARK: - Private Helpers 35 | private extension CurrentCollageContainerViewModel { 36 | 37 | func setupSubscribers() { 38 | PhotoWriter.isAuthorized 39 | .receive(on: DispatchQueue.main) 40 | // .print("CurrentCollageContainerViewModel - PhotoWriter.isAuthorized") 41 | .assign(to: \.isPhotoWriterAuthorized, on: self) 42 | .store(in: &subscriptions) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Resources/image-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/08-photo-collage-app/Collage/Resources/image-1.jpg -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Resources/image-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/08-photo-collage-app/Collage/Resources/image-2.jpg -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Resources/image-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/08-photo-collage-app/Collage/Resources/image-3.jpg -------------------------------------------------------------------------------- /08-photo-collage-app/Collage/Resources/image-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/08-photo-collage-app/Collage/Resources/image-4.jpg -------------------------------------------------------------------------------- /13-resource-management/Playgrounds/MyPlayground.playground/Pages/The Share Operator Pitfalls.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | 7 | var subscriptions = Set() 8 | 9 | let url = URL(string: "https://developer.apple.com/design/human-interface-guidelines")! 10 | 11 | 12 | demo(describing: "Pitfalls of the `share` operator: being too late with future subscriptions") { 13 | 14 | let sharedStream = URLSession.shared 15 | .dataTaskPublisher(for: url) 16 | .map(\.data) 17 | .print("Shared publisher") 18 | .share() 19 | 20 | 21 | print("Subscribing first...") 22 | 23 | sharedStream 24 | .sink( 25 | receiveCompletion: { completion in 26 | print("(subscription 1) - Completion") 27 | }, 28 | receiveValue: { data in 29 | print("(subscription 1) Received data: \(data)") 30 | } 31 | ) 32 | .store(in: &subscriptions) 33 | 34 | 35 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 36 | print("Subscribing second...") 37 | 38 | sharedStream 39 | .sink( 40 | receiveCompletion: { completion in 41 | print("(subscription 2) - Completion") 42 | }, 43 | receiveValue: { data in 44 | print("(subscription 2) Received data: \(data)") 45 | } 46 | ) 47 | .store(in: &subscriptions) 48 | } 49 | } 50 | 51 | 52 | //: [Next](@next) 53 | -------------------------------------------------------------------------------- /13-resource-management/Playgrounds/MyPlayground.playground/Pages/The Share Operator.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | 7 | var subscriptions = Set() 8 | 9 | let url = URL(string: "https://developer.apple.com/design/human-interface-guidelines")! 10 | 11 | 12 | demo(describing: "The `share` operator") { 13 | 14 | let sharedStream = URLSession.shared 15 | .dataTaskPublisher(for: url) 16 | .map(\.data) 17 | .print("Shared publisher") 18 | .share() 19 | 20 | 21 | print("Subscribing first...") 22 | 23 | sharedStream 24 | .sink( 25 | receiveCompletion: { completion in 26 | print("(subscription 1) - Completion") 27 | }, 28 | receiveValue: { data in 29 | print("(subscription 1) Received data: \(data)") 30 | } 31 | ) 32 | .store(in: &subscriptions) 33 | 34 | 35 | 36 | print("Subscribing second...") 37 | 38 | sharedStream 39 | .sink( 40 | receiveCompletion: { completion in 41 | print("(subscription 2) - Completion") 42 | }, 43 | receiveValue: { data in 44 | print("(subscription 2) Received data: \(data)") 45 | } 46 | ) 47 | .store(in: &subscriptions) 48 | 49 | } 50 | 51 | //: [Next](@next) 52 | -------------------------------------------------------------------------------- /13-resource-management/Playgrounds/MyPlayground.playground/Sources/Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public func demo(describing description: String, action: () -> Void) { 5 | print("\n--- Example of: \(description) ---") 6 | action() 7 | } 8 | -------------------------------------------------------------------------------- /13-resource-management/Playgrounds/MyPlayground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CypherPoetNetStack", 6 | "repositoryURL": "https://github.com/CypherPoet/CypherPoetNetStack.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "5c5a1b4bd2e8ace3cada1a4d0358a998e66138a4", 10 | "version": "0.0.20" 11 | } 12 | }, 13 | { 14 | "package": "CypherPoetSwiftUIKit", 15 | "repositoryURL": "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "1a2b0e98984a36f5a480cafd815c386e2f777b1f", 19 | "version": "0.0.22" 20 | } 21 | }, 22 | { 23 | "package": "SatoshiVSKit", 24 | "repositoryURL": "https://github.com/CypherPoet/SatoshiVSKit.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "45a5ab600f192a20c22b3bafe482bd07d919932b", 28 | "version": "0.0.22" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // BitcoinAverageAPIFetcher 4 | // 5 | // Created by Brian Sipple on 11/9/19. 6 | // Copyright © 2019 CypherPoet. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Data/State/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState.swift 3 | // BitcoinAverageAPIFetcher 4 | // 5 | // Created by CypherPoet on 11/9/19. 6 | // ✌️ 7 | // 8 | 9 | import SwiftUI 10 | import CypherPoetSwiftUIKit 11 | 12 | 13 | struct AppState { 14 | var pricesState = PricesState() 15 | var settingsState = SettingsState() 16 | } 17 | 18 | 19 | enum AppAction { 20 | case prices(_ pricesAction: PricesAction) 21 | case settings(_ settingsAction: SettingsAction) 22 | } 23 | 24 | 25 | //enum AppSideEffect: SideEffect {} 26 | 27 | 28 | // MARK: - Reducer 29 | let appReducer = Reducer { appState, action in 30 | switch action { 31 | case let .prices(action): 32 | pricesReducer.reduce(&appState.pricesState, action) 33 | case let .settings(action): 34 | settingsReducer.reduce(&appState.settingsState, action) 35 | } 36 | } 37 | 38 | 39 | typealias AppStore = Store 40 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Data/State/SettingsState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsState.swift 3 | // BitcoinAverageAPIFetcher 4 | // 5 | // Created by CypherPoet on 11/9/19. 6 | // ✌️ 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | import CypherPoetSwiftUIKit 12 | import SatoshiVSKit 13 | 14 | 15 | struct SettingsState { 16 | var filteredShitcoins: [Shitcoin] = [] 17 | } 18 | 19 | 20 | //enum SettingsSideEffect: SideEffect { 21 | // case add(shitcoinToFilters: Shitcoin) 22 | // case remove(shitcoinFromFilters: Shitcoin) 23 | 24 | // 25 | // func mapToAction() -> AnyPublisher { 26 | // switch self { 27 | // case .add(let shitcoinToFilters): 28 | // case .remove(let shitcoinFromFilters): 29 | // } 30 | // } 31 | //} 32 | 33 | 34 | 35 | enum SettingsAction { 36 | case add(shitcoinToFilters: Shitcoin) 37 | case remove(shitcoinFromFilters: Shitcoin) 38 | } 39 | 40 | 41 | 42 | // MARK: - Reducer 43 | let settingsReducer = Reducer { state, action in 44 | switch action { 45 | case .add(let shitcoin): 46 | var filteredShitcoins = state.filteredShitcoins 47 | filteredShitcoins.append(shitcoin) 48 | 49 | state.filteredShitcoins = filteredShitcoins.sorted() 50 | case .remove(let shitcoin): 51 | if let index = state.filteredShitcoins.firstIndex(of: shitcoin) { 52 | state.filteredShitcoins.remove(at: index) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Networking/Dependencies.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dependencies.swift 3 | // BitcoinAverageAPIFetcher 4 | // 5 | // Created by CypherPoet on 11/10/19. 6 | // ✌️ 7 | // 8 | 9 | import Foundation 10 | import SatoshiVSKit 11 | 12 | 13 | enum Dependencies { 14 | static let bitcoinAverageAPIService = BitcoinAverageAPIService( 15 | queue: DispatchQueue(label: "BitcoinAverageAPI", qos: .userInitiated, attributes: [.concurrent]) 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Reusables/Extensions/BitcoinPrice+UpdatedAgoText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BitcoinPrice+UpdatedAgoText.swift 3 | // BitcoinAverageAPIFetcher 4 | // 5 | // Created by CypherPoet on 11/12/19. 6 | // ✌️ 7 | // 8 | 9 | import Foundation 10 | import SatoshiVSKit 11 | 12 | 13 | extension BitcoinPrice { 14 | 15 | func updatedAgoText(offsetFrom currentDate: Date) -> String { 16 | let timeDiff = timestamp - currentDate.timeIntervalSince1970 17 | 18 | return "Last updated \(DateFormatters.priceUpdatedAgo.localizedString(fromTimeInterval: timeDiff))" 19 | } 20 | } 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Reusables/Formatters/DateFormatters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatters.swift 3 | // BitcoinAverageAPIFetcher 4 | // 5 | // Created by CypherPoet on 11/11/19. 6 | // ✌️ 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | enum DateFormatters { 13 | static let priceReadingTime: DateFormatter = { 14 | let formatter = DateFormatter() 15 | 16 | formatter.timeZone = .current 17 | formatter.timeStyle = .medium 18 | 19 | return formatter 20 | }() 21 | 22 | 23 | static let priceReadingTimeBadge: DateFormatter = { 24 | let formatter = DateFormatter() 25 | 26 | formatter.dateStyle = .none 27 | formatter.timeStyle = .short 28 | 29 | return formatter 30 | }() 31 | 32 | 33 | static let priceUpdatedAgo = RelativeDateTimeFormatter() 34 | } 35 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Reusables/Formatters/NumberFormatters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberFormatters.swift 3 | // BitcoinAverageAPIFetcher 4 | // 5 | // Created by CypherPoet on 11/11/19. 6 | // ✌️ 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | enum NumberFormatters { 13 | static let priceReading: NumberFormatter = { 14 | let formatter = NumberFormatter() 15 | 16 | formatter.numberStyle = .decimal 17 | formatter.maximumFractionDigits = 10 18 | 19 | return formatter 20 | }() 21 | } 22 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Reusables/Views/ShitcoinFilterListItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShitcoinFilterListItem.swift 3 | // BitcoinAverageAPIFetcher 4 | // 5 | // Created by CypherPoet on 11/16/19. 6 | // ✌️ 7 | // 8 | 9 | import SwiftUI 10 | import SatoshiVSKit 11 | 12 | 13 | struct ShitcoinFilterListItem: View { 14 | let shitcoin: Shitcoin 15 | let isSelected: Bool 16 | 17 | let onSelectionToggled: ((Shitcoin, Bool) -> Void) 18 | } 19 | 20 | 21 | // MARK: - Body 22 | extension ShitcoinFilterListItem { 23 | 24 | var body: some View { 25 | Button(action: { 26 | self.onSelectionToggled(self.shitcoin, !self.isSelected) 27 | }) { 28 | HStack { 29 | Text(shitcoin.name) 30 | 31 | Spacer() 32 | 33 | if isSelected { 34 | Image(systemName: "checkmark") 35 | .imageScale(.large) 36 | .foregroundColor(.green) 37 | .transition(.opacity) 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | 45 | // MARK: - Computeds 46 | extension ShitcoinFilterListItem { 47 | } 48 | 49 | 50 | // MARK: - View Variables 51 | extension ShitcoinFilterListItem { 52 | } 53 | 54 | 55 | 56 | // MARK: - Preview 57 | struct ShitcoinFilterListItem_Previews: PreviewProvider { 58 | 59 | static var previews: some View { 60 | ShitcoinFilterListItem(shitcoin: .ada, isSelected: true) { (_, _) in } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Reusables/Views/TimestampBadge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimestampBadge.swift 3 | // BitcoinAverageAPIFetcher 4 | // 5 | // Created by CypherPoet on 11/11/19. 6 | // ✌️ 7 | // 8 | 9 | import SwiftUI 10 | 11 | 12 | struct TimestampBadge: View { 13 | let timeValue: Date 14 | } 15 | 16 | 17 | // MARK: - Body 18 | extension TimestampBadge { 19 | 20 | var body: some View { 21 | Text("\(timeValue, formatter: DateFormatters.priceReadingTimeBadge)") 22 | .font(.headline) 23 | .fontWeight(.heavy) 24 | .padding(10) 25 | .foregroundColor(.white) 26 | .background(Color.orange) 27 | .frame(idealWidth: 100) 28 | .cornerRadius(8) 29 | } 30 | } 31 | 32 | 33 | // MARK: - Computeds 34 | extension TimestampBadge { 35 | 36 | 37 | } 38 | 39 | 40 | // MARK: - View Variables 41 | extension TimestampBadge { 42 | 43 | 44 | } 45 | 46 | 47 | 48 | // MARK: - Preview 49 | struct TimestampBadge_Previews: PreviewProvider { 50 | 51 | static var previews: some View { 52 | TimestampBadge(timeValue: Date()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Scenes/Settings/SettingsContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsContainerView.swift 3 | // BitcoinAverageAPIFetcher 4 | // 5 | // Created by CypherPoet on 11/15/19. 6 | // ✌️ 7 | // 8 | 9 | import SwiftUI 10 | 11 | 12 | struct SettingsContainerView: View { 13 | @EnvironmentObject private var store: AppStore 14 | @State private var isShowingFilterPicker = false 15 | } 16 | 17 | 18 | // MARK: - Body 19 | extension SettingsContainerView { 20 | 21 | var body: some View { 22 | NavigationView { 23 | SettingsView() 24 | .navigationBarItems(trailing: addFilterButton) 25 | .sheet(isPresented: $isShowingFilterPicker) { 26 | ShitcoinFilterSelectionView() 27 | .environmentObject(self.store) 28 | } 29 | } 30 | } 31 | } 32 | 33 | 34 | // MARK: - Computeds 35 | extension SettingsContainerView { 36 | 37 | 38 | } 39 | 40 | 41 | // MARK: - View Variables 42 | extension SettingsContainerView { 43 | 44 | private var addFilterButton: some View { 45 | Button(action: { 46 | self.isShowingFilterPicker = true 47 | }) { 48 | Text("Add Filter") 49 | } 50 | } 51 | 52 | } 53 | 54 | 55 | 56 | // MARK: - Preview 57 | struct SettingsContainerView_Previews: PreviewProvider { 58 | 59 | static var previews: some View { 60 | SettingsContainerView() 61 | .environmentObject(SampleStore.default) 62 | .environmentObject(SettingsViewModel(store: SampleStore.default)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Scenes/Settings/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // BitcoinAverageAPIFetcher 4 | // 5 | // Created by CypherPoet on 11/10/19. 6 | // ✌️ 7 | // 8 | 9 | import SwiftUI 10 | 11 | 12 | struct SettingsView: View { 13 | @EnvironmentObject var viewModel: SettingsViewModel 14 | } 15 | 16 | 17 | // MARK: - Body 18 | extension SettingsView { 19 | 20 | var body: some View { 21 | List { 22 | Section(header: Text("Filtered Shitcoins")) { 23 | ForEach(viewModel.filteredShitcoins) { shitcoin in 24 | Text(shitcoin.name) 25 | } 26 | } 27 | } 28 | .navigationBarTitle("Settings", displayMode: .large) 29 | } 30 | } 31 | 32 | 33 | // MARK: - Computeds 34 | extension SettingsView { 35 | 36 | 37 | } 38 | 39 | 40 | // MARK: - View Variables 41 | extension SettingsView { 42 | 43 | 44 | } 45 | 46 | 47 | 48 | // MARK: - Preview 49 | struct SettingsView_Previews: PreviewProvider { 50 | 51 | static var previews: some View { 52 | SettingsView() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /15-19-bitcoin-average-api-app/BitcoinAverageAPIFetcher/BitcoinAverageAPIFetcher/Scenes/Settings/SettingsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewModel.swift 3 | // BitcoinAverageAPIFetcher 4 | // 5 | // Created by CypherPoet on 11/15/19. 6 | // ✌️ 7 | // 8 | 9 | 10 | import SwiftUI 11 | import Combine 12 | import SatoshiVSKit 13 | 14 | 15 | final class SettingsViewModel: ObservableObject { 16 | private var subscriptions = Set() 17 | 18 | let store: AppStore 19 | 20 | 21 | // MARK: - Published Properties 22 | @Published var filteredShitcoins: [Shitcoin] = [] 23 | 24 | 25 | // MARK: - Init 26 | init(store: AppStore) { 27 | self.store = store 28 | 29 | setupSubscribers() 30 | } 31 | } 32 | 33 | 34 | // MARK: - Publishers 35 | extension SettingsViewModel { 36 | 37 | private var settingsStatePublisher: AnyPublisher { 38 | store.$state 39 | .map(\.settingsState) 40 | .eraseToAnyPublisher() 41 | } 42 | 43 | 44 | private var filteredShitcoinsPublisher: AnyPublisher<[Shitcoin], Never> { 45 | settingsStatePublisher 46 | .map(\.filteredShitcoins) 47 | .eraseToAnyPublisher() 48 | } 49 | } 50 | 51 | 52 | // MARK: - Computeds 53 | extension SettingsViewModel { 54 | } 55 | 56 | 57 | // MARK: - Public Methods 58 | extension SettingsViewModel { 59 | } 60 | 61 | 62 | 63 | // MARK: - Private Helpers 64 | private extension SettingsViewModel { 65 | 66 | func setupSubscribers() { 67 | filteredShitcoinsPublisher 68 | .receive(on: DispatchQueue.main) 69 | .assign(to: \.filteredShitcoins, on: self) 70 | .store(in: &subscriptions) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /16-error-handling/Playgrounds/MyPlayground.playground/Pages/AssertNoFailure.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | 7 | //: ## AssertNoFailure 8 | 9 | //: The `assertNoFailure` operator is useful when you want to protect 10 | //: yourself during development and confirm a publisher can't finish with a failure event. 11 | //: 12 | //: It doesn't prevent a failure event from being emitted by the upstream. 13 | //: However, it will crash with a fatalError if it detects an error, which 14 | //: gives you a good incentive to fix it in development. 15 | 16 | 17 | var subscriptions = Set() 18 | 19 | 20 | enum MyError: Error { 21 | case oops 22 | } 23 | 24 | 25 | demo(describing: "assertNoFailure") { 26 | Just("🚀") 27 | .setFailureType(to: MyError.self) 28 | // .tryMap { _ in throw MyError.oops } // 📝 Uncomment this code to fail 29 | .assertNoFailure() 30 | .sink( 31 | receiveValue: { print($0) } 32 | ) 33 | .store(in: &subscriptions) 34 | } 35 | 36 | //: [Next](@next) 37 | -------------------------------------------------------------------------------- /16-error-handling/Playgrounds/MyPlayground.playground/Pages/Assign.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | 7 | //: ## Assign 8 | 9 | var subscriptions = Set() 10 | 11 | 12 | 13 | class Player { 14 | var name: String = "Unknown" 15 | var xp: Double = 0.0 16 | } 17 | 18 | 19 | 20 | demo(describing: "assign") { 21 | let player = Player() 22 | 23 | print("Player name before assignment: \(player.name)") 24 | 25 | Just("CypherPoet") 26 | // .setFailureType(to: Error.self) 27 | .handleEvents( 28 | receiveCompletion: { _ in print("completion") } 29 | ) 30 | .assign(to: \.name, on: player) 31 | .store(in: &subscriptions) 32 | 33 | 34 | print("Player name after assignment: \(player.name)") 35 | 36 | } 37 | 38 | //: [Next](@next) 39 | -------------------------------------------------------------------------------- /16-error-handling/Playgrounds/MyPlayground.playground/Pages/Never.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | 7 | //: ## Never 8 | 9 | var subscriptions = Set() 10 | 11 | 12 | enum CustomError: Error { 13 | case ohNo 14 | case oopsieDaisy 15 | } 16 | 17 | 18 | demo(describing: "Setting a failure type for a `Never` stream") { 19 | Just("⚡️") 20 | .setFailureType(to: CustomError.self) 21 | .eraseToAnyPublisher() 22 | .sink( 23 | receiveCompletion: { (completion) in 24 | switch completion { 25 | case .failure(.ohNo): 26 | print("Oh No!") 27 | case .failure(.oopsieDaisy): 28 | print("Whaaa???!!!") 29 | case .finished: 30 | print("Finished successfully!") 31 | } 32 | }, 33 | receiveValue: { print($0) } 34 | ) 35 | .store(in: &subscriptions) 36 | } 37 | 38 | //: [Next](@next) 39 | -------------------------------------------------------------------------------- /16-error-handling/Playgrounds/MyPlayground.playground/Resources/hq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/16-error-handling/Playgrounds/MyPlayground.playground/Resources/hq.jpg -------------------------------------------------------------------------------- /16-error-handling/Playgrounds/MyPlayground.playground/Resources/lq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/16-error-handling/Playgrounds/MyPlayground.playground/Resources/lq.jpg -------------------------------------------------------------------------------- /16-error-handling/Playgrounds/MyPlayground.playground/Resources/na.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/16-error-handling/Playgrounds/MyPlayground.playground/Resources/na.jpg -------------------------------------------------------------------------------- /16-error-handling/Playgrounds/MyPlayground.playground/Sources/Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public func demo(describing description: String, action: () -> Void) { 5 | print("\n--- Example of: \(description) ---") 6 | action() 7 | } 8 | -------------------------------------------------------------------------------- /16-error-handling/Playgrounds/MyPlayground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /17-schedulers/Playgrounds/MyPlayground.playground/Pages/Challenge 1 - Stop the Timer.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | import PlaygroundSupport 6 | 7 | //: ## Challenge 1: Stop the Timer 8 | //: 9 | //: In this chapter’s section about DispatchQueue you created a cancellable 10 | //: timer to feed your source publisher with values. 11 | //: 12 | //: Devise two different ways of stopping the timer after 4 seconds. 13 | 14 | 15 | let incrementer = Timer 16 | .publish(every: 1.0, on: .main, in: .common) 17 | .autoconnect() 18 | .scan(0) { (accumulatedCount, _) in 19 | accumulatedCount + 1 20 | } 21 | 22 | let eventQueue = DispatchQueue(label: "Custom Serial Queue", qos: .userInitiated) 23 | 24 | 25 | 26 | let numberPublisher = incrementer 27 | .receive(on: eventQueue) 28 | .eraseToAnyPublisher() 29 | 30 | 31 | let subscription = numberPublisher.sink(receiveValue: { print($0) }) 32 | 33 | 34 | // MARK: - Solution 1 35 | //eventQueue.schedule( 36 | // after: eventQueue.now.advanced(by: .seconds(4)), 37 | // tolerance: .seconds(0.1) 38 | //) { 39 | // subscription.cancel() 40 | //} 41 | 42 | 43 | // MARK: - Solution 2 44 | 45 | eventQueue.asyncAfter(deadline: .now() + 4) { 46 | subscription.cancel() 47 | } 48 | 49 | //: [Next](@next) 50 | -------------------------------------------------------------------------------- /17-schedulers/Playgrounds/MyPlayground.playground/Pages/ImmediateScheduler.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | //: ## ImmediateScheduler 4 | 5 | 6 | import Foundation 7 | import Combine 8 | import PlaygroundSupport 9 | import SwiftUI 10 | 11 | 12 | var subscriptions = Set() 13 | 14 | let computationPublisher = Publishers.ExpensiveComputation(duration: 3) 15 | let customQueue = DispatchQueue(label: "Serial queue") 16 | 17 | let startingThreadNumber = Thread.current.number 18 | 19 | 20 | let incrementer = Timer 21 | .publish(every: 1.0, on: .main, in: .common) 22 | .autoconnect() 23 | .scan(0) { counter, _ in 24 | counter + 1 25 | } 26 | 27 | 28 | //: The `ImmediateScheduler` “schedules” immediately on the current thread. 29 | 30 | demo(describing: "ImmediateScheduler") { 31 | let setupPublisher = { recorder in 32 | incrementer 33 | .receive(on: DispatchQueue.global()) 34 | .recordThread(using: recorder) 35 | .receive(on: ImmediateScheduler.shared) 36 | // .receive(on: customQueue) 37 | .recordThread(using: recorder) 38 | .eraseToAnyPublisher() 39 | } 40 | 41 | let view = ThreadRecorderView(title: "Using ImmediateScheduler", setup: setupPublisher) 42 | 43 | PlaygroundPage.current.liveView = UIHostingController(rootView: view) 44 | } 45 | 46 | //: [Next](@next) 47 | -------------------------------------------------------------------------------- /17-schedulers/Playgrounds/MyPlayground.playground/Sources/PlaygroundUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public func demo(describing description: String, action: () -> Void) { 5 | print("\n--- Example of: \(description) ---") 6 | action() 7 | } 8 | -------------------------------------------------------------------------------- /17-schedulers/Playgrounds/MyPlayground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /18-custom-publishers-and-handling-backpressure/Playgrounds/MyPlayground.playground/Pages/Unwrap Operator.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | 7 | var subscriptions = Set() 8 | 9 | 10 | extension Publisher { 11 | 12 | func unwrap() -> Publishers.CompactMap 13 | where Output == Optional 14 | { 15 | compactMap { $0 } 16 | } 17 | } 18 | 19 | 20 | demo(describing: "Using our custom `unwrap` operator") { 21 | let numbers: [Int?] = [1, 1, 2, 3, nil, nil, 13] 22 | 23 | numbers 24 | .publisher 25 | .unwrap() 26 | .sink( 27 | receiveCompletion: { completion in 28 | print("Received completion: \(completion)") 29 | }, 30 | receiveValue: { value in 31 | print("Received value: \(value)") 32 | } 33 | ) 34 | .store(in: &subscriptions) 35 | } 36 | 37 | //: [Next](@next) 38 | -------------------------------------------------------------------------------- /18-custom-publishers-and-handling-backpressure/Playgrounds/MyPlayground.playground/Sources/PlaygroundUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public func demo(describing description: String, action: () -> Void) { 5 | print("\n--- Example of: \(description) ---") 6 | action() 7 | } 8 | -------------------------------------------------------------------------------- /18-custom-publishers-and-handling-backpressure/Playgrounds/MyPlayground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /19-testing-combine-code/Projects/ColorCalc/ColorCalc.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /19-testing-combine-code/Projects/ColorCalc/ColorCalc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /19-testing-combine-code/Projects/ColorCalc/ColorCalc.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CypherPoetCombineKit", 6 | "repositoryURL": "https://github.com/CypherPoet/CypherPoetCombineKit.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "72bf0a79055af30e2975bcf4e0f3c6b5d51fe429", 10 | "version": "0.0.3" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /19-testing-combine-code/Projects/ColorCalc/ColorCalc/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /19-testing-combine-code/Projects/ColorCalc/ColorCalc/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 | -------------------------------------------------------------------------------- /19-testing-combine-code/Projects/ColorCalc/ColorCalc/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /19-testing-combine-code/Projects/ColorCalc/ColorCalcTests/ Combine Operators/CombineOperatorsTests+Collect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineOperatorsTests+Collect.swift 3 | // ColorCalc 4 | // 5 | // Created by CypherPoet on 12/20/19. 6 | // ✌️ 7 | // 8 | 9 | import XCTest 10 | import Combine 11 | 12 | 13 | extension CombineOperatorsTests { 14 | 15 | /// The `collect` operator will buffer values emitted by an upstream publisher, 16 | /// wait for it to complete, and then emit an array containing those values downstream. 17 | func testCollect() { 18 | let basePublisher = PassthroughSubject() 19 | let fibs = [1, 1, 2, 3, 5, 8, 13, 21] 20 | let expectation = XCTestExpectation(description: "Publisher should complete successfully.") 21 | 22 | 23 | basePublisher 24 | .collect() 25 | .sink( 26 | receiveCompletion: { completion in 27 | switch completion { 28 | case .finished: 29 | expectation.fulfill() 30 | case .failure: 31 | XCTFail() 32 | } 33 | }, 34 | receiveValue: { numbers in 35 | XCTAssertEqual(numbers, fibs) 36 | } 37 | ) 38 | .store(in: &subscriptions) 39 | 40 | fibs.forEach { basePublisher.send($0) } 41 | basePublisher.send(completion: .finished) 42 | 43 | wait(for: [expectation], timeout: 2.0) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /19-testing-combine-code/Projects/ColorCalc/ColorCalcTests/ Combine Operators/CombineOperatorsTests+ShareReplay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineOperatorsTests+ShareReplay.swift 3 | // ColorCalc 4 | // 5 | // Created by CypherPoet on 12/21/19. 6 | // ✌️ 7 | // 8 | 9 | import XCTest 10 | import Combine 11 | import CypherPoetCombineKit_ShareReplay 12 | 13 | 14 | extension CombineOperatorsTests { 15 | 16 | func testShareReplay() { 17 | let basePublisher = PassthroughSubject() 18 | let numberStream = basePublisher.shareReplay(capacity: 2) 19 | 20 | let expectation = XCTestExpectation(description: "Publisher should complete successfully.") 21 | let expectedValues = [1, 2, 3, 4, 3, 4, 5, 5] 22 | var receivedValues: [Int] = [] 23 | 24 | 25 | numberStream 26 | .sink(receiveValue: { receivedValues.append($0) }) 27 | .store(in: &subscriptions) 28 | 29 | 30 | basePublisher.send(1) 31 | basePublisher.send(2) 32 | basePublisher.send(3) 33 | basePublisher.send(4) 34 | 35 | 36 | numberStream 37 | .sink(receiveValue: { receivedValues.append($0) }) 38 | .store(in: &subscriptions) 39 | 40 | 41 | basePublisher.send(5) 42 | 43 | XCTAssertEqual(receivedValues, expectedValues) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /19-testing-combine-code/Projects/ColorCalc/ColorCalcTests/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 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/Colors/Gray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "extended-srgb", 11 | "components" : { 12 | "red" : "0.949", 13 | "alpha" : "1.000", 14 | "blue" : "0.969", 15 | "green" : "0.949" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "dark" 25 | } 26 | ], 27 | "color" : { 28 | "color-space" : "srgb", 29 | "components" : { 30 | "red" : "0.574", 31 | "alpha" : "1.000", 32 | "blue" : "0.574", 33 | "green" : "0.574" 34 | } 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/Colors/Green.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "display-p3", 11 | "components" : { 12 | "red" : "0.200", 13 | "alpha" : "1.000", 14 | "blue" : "0.000", 15 | "green" : "0.800" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/Colors/Red.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "1.000", 13 | "alpha" : "1.000", 14 | "blue" : "0.474", 15 | "green" : "0.493" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/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 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Models/ChuckNorrisJokes.xcdatamodeld/ChuckNorrisJokes.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokes/SampleJoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "dev" 4 | ], 5 | "created_at": "2016-05-01 10:51:41.584544", 6 | "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", 7 | "id": "mgdb9q1wqb6_gurzp_5bga", 8 | "updated_at": "2016-05-01 10:51:41.584544", 9 | "url": "https:\/\/api.chucknorris.io\/jokes\/mgdb9q1wqb6_gurzp_5bga", 10 | "value": "Chuck Norris writes code that optimizes itself." 11 | } 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokesModel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokesTests/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 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokesTests/TestJoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "dev" 4 | ], 5 | "created_at": "2016-05-01 10:51:41.584544", 6 | "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", 7 | "id": "ag_6paerrkg-mxfjjqw4ba", 8 | "updated_at": "2016-05-01 10:51:41.584544", 9 | "url": "https:\/\/api.chucknorris.io\/jokes\/ag_6paerrkg-mxfjjqw4ba", 10 | "value": "Chuck Norris's beard can type 140 wpm." 11 | } 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokesTests/TestTranslatedJoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "dev" 4 | ], 5 | "created_at": "2016-05-01 10:51:41.584544", 6 | "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", 7 | "id": "ag_6paerrkg-mxfjjqw4ba", 8 | "updated_at": "2016-05-01 10:51:41.584544", 9 | "url": "https:\/\/api.chucknorris.io\/jokes\/ag_6paerrkg-mxfjjqw4ba", 10 | "value": "La barba de Chuck Norris puede escribir 140 wpm." 11 | } 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/final/ChuckNorrisJokesTests/TestTranslationResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "lang": "en-es", 4 | "text": ["La barba de Chuck Norris puede escribir 140 wpm."] 5 | } 6 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/Colors/Gray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "extended-srgb", 11 | "components" : { 12 | "red" : "0.949", 13 | "alpha" : "1.000", 14 | "blue" : "0.969", 15 | "green" : "0.949" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "dark" 25 | } 26 | ], 27 | "color" : { 28 | "color-space" : "srgb", 29 | "components" : { 30 | "red" : "0.574", 31 | "alpha" : "1.000", 32 | "blue" : "0.574", 33 | "green" : "0.574" 34 | } 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/Colors/Green.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "display-p3", 11 | "components" : { 12 | "red" : "0.200", 13 | "alpha" : "1.000", 14 | "blue" : "0.000", 15 | "green" : "0.800" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/Colors/Red.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "1.000", 13 | "alpha" : "1.000", 14 | "blue" : "0.474", 15 | "green" : "0.493" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/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 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Models/ChuckNorrisJokes.xcdatamodeld/ChuckNorrisJokes.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokes/SampleJoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "dev" 4 | ], 5 | "created_at": "2016-05-01 10:51:41.584544", 6 | "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", 7 | "id": "mgdb9q1wqb6_gurzp_5bga", 8 | "updated_at": "2016-05-01 10:51:41.584544", 9 | "url": "https:\/\/api.chucknorris.io\/jokes\/mgdb9q1wqb6_gurzp_5bga", 10 | "value": "Chuck Norris writes code that optimizes itself." 11 | } 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokesModel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokesTests/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 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokesTests/TestJoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "dev" 4 | ], 5 | "created_at": "2016-05-01 10:51:41.584544", 6 | "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", 7 | "id": "ag_6paerrkg-mxfjjqw4ba", 8 | "updated_at": "2016-05-01 10:51:41.584544", 9 | "url": "https:\/\/api.chucknorris.io\/jokes\/ag_6paerrkg-mxfjjqw4ba", 10 | "value": "Chuck Norris's beard can type 140 wpm." 11 | } 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokesTests/TestTranslatedJoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "dev" 4 | ], 5 | "created_at": "2016-05-01 10:51:41.584544", 6 | "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", 7 | "id": "ag_6paerrkg-mxfjjqw4ba", 8 | "updated_at": "2016-05-01 10:51:41.584544", 9 | "url": "https:\/\/api.chucknorris.io\/jokes\/ag_6paerrkg-mxfjjqw4ba", 10 | "value": "La barba de Chuck Norris puede escribir 140 wpm." 11 | } 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/challenge/starter/ChuckNorrisJokesTests/TestTranslationResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "lang": "en-es", 4 | "text": ["La barba de Chuck Norris puede escribir 140 wpm."] 5 | } 6 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/Colors/Gray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "extended-srgb", 11 | "components" : { 12 | "red" : "0.949", 13 | "alpha" : "1.000", 14 | "blue" : "0.969", 15 | "green" : "0.949" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "dark" 25 | } 26 | ], 27 | "color" : { 28 | "color-space" : "srgb", 29 | "components" : { 30 | "red" : "0.574", 31 | "alpha" : "1.000", 32 | "blue" : "0.574", 33 | "green" : "0.574" 34 | } 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/Colors/Green.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "display-p3", 11 | "components" : { 12 | "red" : "0.200", 13 | "alpha" : "1.000", 14 | "blue" : "0.000", 15 | "green" : "0.800" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/Colors/Red.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "1.000", 13 | "alpha" : "1.000", 14 | "blue" : "0.474", 15 | "green" : "0.493" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/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 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Models/ChuckNorrisJokes.xcdatamodeld/ChuckNorrisJokes.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokes/SampleJoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "dev" 4 | ], 5 | "created_at": "2016-05-01 10:51:41.584544", 6 | "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", 7 | "id": "mgdb9q1wqb6_gurzp_5bga", 8 | "updated_at": "2016-05-01 10:51:41.584544", 9 | "url": "https:\/\/api.chucknorris.io\/jokes\/mgdb9q1wqb6_gurzp_5bga", 10 | "value": "Chuck Norris writes code that optimizes itself." 11 | } 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokesModel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokesTests/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 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokesTests/TestJoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "dev" 4 | ], 5 | "created_at": "2016-05-01 10:51:41.584544", 6 | "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", 7 | "id": "ag_6paerrkg-mxfjjqw4ba", 8 | "updated_at": "2016-05-01 10:51:41.584544", 9 | "url": "https:\/\/api.chucknorris.io\/jokes\/ag_6paerrkg-mxfjjqw4ba", 10 | "value": "Chuck Norris's beard can type 140 wpm." 11 | } 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokesTests/TestTranslatedJoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "dev" 4 | ], 5 | "created_at": "2016-05-01 10:51:41.584544", 6 | "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", 7 | "id": "ag_6paerrkg-mxfjjqw4ba", 8 | "updated_at": "2016-05-01 10:51:41.584544", 9 | "url": "https:\/\/api.chucknorris.io\/jokes\/ag_6paerrkg-mxfjjqw4ba", 10 | "value": "La barba de Chuck Norris puede escribir 140 wpm." 11 | } 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/final/ChuckNorrisJokesTests/TestTranslationResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "lang": "en-es", 4 | "text": ["La barba de Chuck Norris puede escribir 140 wpm."] 5 | } 6 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/Colors/Gray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "extended-srgb", 11 | "components" : { 12 | "red" : "0.949", 13 | "alpha" : "1.000", 14 | "blue" : "0.969", 15 | "green" : "0.949" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "dark" 25 | } 26 | ], 27 | "color" : { 28 | "color-space" : "srgb", 29 | "components" : { 30 | "red" : "0.574", 31 | "alpha" : "1.000", 32 | "blue" : "0.574", 33 | "green" : "0.574" 34 | } 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/Colors/Green.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "display-p3", 11 | "components" : { 12 | "red" : "0.200", 13 | "alpha" : "1.000", 14 | "blue" : "0.000", 15 | "green" : "0.800" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/Colors/Red.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "1.000", 13 | "alpha" : "1.000", 14 | "blue" : "0.474", 15 | "green" : "0.493" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/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 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Models/ChuckNorrisJokes.xcdatamodeld/ChuckNorrisJokes.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokes/SampleJoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "dev" 4 | ], 5 | "created_at": "2016-05-01 10:51:41.584544", 6 | "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", 7 | "id": "mgdb9q1wqb6_gurzp_5bga", 8 | "updated_at": "2016-05-01 10:51:41.584544", 9 | "url": "https:\/\/api.chucknorris.io\/jokes\/mgdb9q1wqb6_gurzp_5bga", 10 | "value": "Chuck Norris writes code that optimizes itself." 11 | } 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokesModel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokesTests/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 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokesTests/TestJoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "dev" 4 | ], 5 | "created_at": "2016-05-01 10:51:41.584544", 6 | "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", 7 | "id": "ag_6paerrkg-mxfjjqw4ba", 8 | "updated_at": "2016-05-01 10:51:41.584544", 9 | "url": "https:\/\/api.chucknorris.io\/jokes\/ag_6paerrkg-mxfjjqw4ba", 10 | "value": "Chuck Norris's beard can type 140 wpm." 11 | } 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokesTests/TestTranslatedJoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "dev" 4 | ], 5 | "created_at": "2016-05-01 10:51:41.584544", 6 | "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", 7 | "id": "ag_6paerrkg-mxfjjqw4ba", 8 | "updated_at": "2016-05-01 10:51:41.584544", 9 | "url": "https:\/\/api.chucknorris.io\/jokes\/ag_6paerrkg-mxfjjqw4ba", 10 | "value": "La barba de Chuck Norris puede escribir 140 wpm." 11 | } 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/BookExample/starter/ChuckNorrisJokesTests/TestTranslationResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "lang": "en-es", 4 | "text": ["La barba de Chuck Norris puede escribir 140 wpm."] 5 | } 6 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Common/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Common/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Common/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Common", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15), 11 | ], 12 | products: [ 13 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 14 | .library( 15 | name: "Common", 16 | targets: [ 17 | "Common", 18 | ] 19 | ), 20 | ], 21 | dependencies: [ 22 | // Dependencies declare other packages that this package depends on. 23 | // .package(url: /* package url */, from: "1.0.0"), 24 | .package(url: "https://github.com/CypherPoet/CypherPoetCoreDataKit.git", from: "0.0.11"), 25 | ], 26 | targets: [ 27 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 28 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 29 | .target( 30 | name: "Common", 31 | dependencies: [ 32 | "CypherPoetCoreDataKit" 33 | ], 34 | path: "Sources/" 35 | ), 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Common/README.md: -------------------------------------------------------------------------------- 1 | # Common 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Common/Sources/Models/Language/Language.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public enum Language: String { 5 | case english 6 | case french 7 | case spanish 8 | } 9 | 10 | 11 | extension Language: CaseIterable {} 12 | 13 | extension Language: Identifiable { 14 | public var id: String { code } 15 | } 16 | 17 | 18 | 19 | extension Language { 20 | 21 | public init?(code: String) { 22 | guard let language = Self.allCases.first(where: { $0.code == code }) else { 23 | return nil 24 | } 25 | 26 | self = language 27 | } 28 | 29 | 30 | public var shortName: String { 31 | switch self { 32 | case .english: 33 | return "EN" 34 | case .spanish: 35 | return "ES" 36 | case .french: 37 | return "FR" 38 | } 39 | } 40 | 41 | 42 | public var longName: String { 43 | switch self { 44 | case .english: 45 | return "English" 46 | case .spanish: 47 | return "Spanish" 48 | case .french: 49 | return "French" 50 | } 51 | } 52 | 53 | 54 | public var code: String { 55 | switch self { 56 | case .english: 57 | return "en" 58 | case .spanish: 59 | return "es" 60 | case .french: 61 | return "fr" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Common/Sources/Models/NumberFact/NumberFact+Category.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | extension NumberFact { 5 | 6 | public enum Category: String { 7 | case math 8 | case trivia 9 | case date 10 | case year 11 | } 12 | } 13 | 14 | 15 | extension NumberFact.Category: CaseIterable {} 16 | extension NumberFact.Category: Identifiable { 17 | public var id: String { rawValue } 18 | } 19 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Common/Sources/Models/NumberFact/NumberFact+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreData 3 | 4 | @objc(NumberFact) 5 | public class NumberFact: NSManagedObject { 6 | 7 | } 8 | 9 | extension NumberFact: Identifiable {} 10 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Common/Sources/Models/NumberFact/NumberFact+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreData 3 | 4 | 5 | extension NumberFact { 6 | @NSManaged public var number: Int64 7 | @NSManaged public var categoryValue: String 8 | @NSManaged public var text: String 9 | @NSManaged public var translatedText: String? 10 | @NSManaged public var currentLanguageValue: String 11 | @NSManaged public var translationLanguageValue: String 12 | @NSManaged public var isFavorite: Bool 13 | 14 | 15 | public var category: NumberFact.Category { 16 | get { NumberFact.Category(rawValue: categoryValue)! } 17 | set { categoryValue = newValue.rawValue } 18 | } 19 | 20 | 21 | public var currentLanguage: Language { 22 | get { Language(rawValue: currentLanguageValue)! } 23 | set { currentLanguageValue = newValue.rawValue } 24 | } 25 | 26 | 27 | public var translationLanguage: Language { 28 | get { Language(rawValue: translationLanguageValue)! } 29 | set { translationLanguageValue = newValue.rawValue } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Common/Sources/Models/NumberFact/NumberFact+Decoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NumberFact { 4 | public enum Decoder { 5 | public static let `default` = JSONDecoder() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Common/Sources/SupportedLanguages.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Brian Sipple on 2/20/20. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Design/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CypherPoet/book--combine-asynchronous-programming-with-swift/186a0c9b5a4e9cb14f0ca415b9588e82eccf01f8/20-building-a-complete-app/NumberFacts/Design/screenshot-1.png -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CypherPoetCoreDataKit", 6 | "repositoryURL": "https://github.com/CypherPoet/CypherPoetCoreDataKit.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "19e26a32e1cde119c41ceb50a59159fa94c5e1ae", 10 | "version": "0.0.11" 11 | } 12 | }, 13 | { 14 | "package": "CypherPoetNetStack", 15 | "repositoryURL": "https://github.com/CypherPoet/CypherPoetNetStack.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "b5dbd717cd51726ccba1041ee5981680aaeff95f", 19 | "version": "0.0.28" 20 | } 21 | }, 22 | { 23 | "package": "CypherPoetSwiftUIKit", 24 | "repositoryURL": "https://github.com/CypherPoet/CypherPoetSwiftUIKit.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "ae3c4bf2c35bd329ba0b6c185ad3938d92b41bfe", 28 | "version": "0.0.41" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // NumberFacts 4 | // 5 | // Created by Brian Sipple on 2/14/20. 6 | // Copyright © 2020 CypherPoet. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreData 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | CurrentApp.coreDataManager.setup() 19 | 20 | return true 21 | } 22 | 23 | 24 | // MARK: UISceneSession Lifecycle 25 | 26 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 27 | // Called when a new scene session is being created. 28 | // Use this method to select a configuration to create the new scene with. 29 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 30 | } 31 | 32 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 33 | // Called when the user discards a scene session. 34 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 35 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/App/CurrentApplication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurrentApplication.swift 3 | // NumberFacts 4 | // 5 | // Created by CypherPoet on 2/18/20. 6 | // ✌️ 7 | // 8 | 9 | import Foundation 10 | import Common 11 | import CypherPoetCoreDataKit_CoreDataManager 12 | 13 | 14 | struct CurrentApplication { 15 | var coreDataManager: CoreDataManager 16 | var defaultLanguage: Language 17 | } 18 | 19 | 20 | var CurrentApp = CurrentApplication( 21 | coreDataManager: .shared, 22 | defaultLanguage: Language(code: Locale.current.languageCode ?? "") ?? .english 23 | ) 24 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/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 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Data/CoreDataManager+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataManager+Utils.swift 3 | // NumberFacts 4 | // 5 | // Created by CypherPoet on 2/18/20. 6 | // ✌️ 7 | // 8 | 9 | import Foundation 10 | import CypherPoetCoreDataKit_CoreDataManager 11 | 12 | 13 | extension CoreDataManager { 14 | static let shared = CoreDataManager( 15 | managedObjectModelName: "NumberFacts" 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Data/NumberFacts.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | NumberFacts.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Data/NumberFacts.xcdatamodeld/NumberFacts.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Data/State/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState.swift 3 | // NumberFacts 4 | // 5 | // Created by CypherPoet on 2/18/20. 6 | // ✌️ 7 | // 8 | 9 | 10 | import Foundation 11 | import Combine 12 | import CypherPoetSwiftUIKit_DataFlowUtils 13 | 14 | 15 | struct AppState { 16 | var numberFactsState = NumberFactsState() 17 | } 18 | 19 | 20 | 21 | //enum AppSideEffect: SideEffect { 22 | // 23 | //} 24 | 25 | 26 | 27 | enum AppAction { 28 | case numberFacts(_ action: NumberFactsAction) 29 | } 30 | 31 | 32 | // MARK: - Reducer 33 | let appReducer: Reducer = Reducer( 34 | reduce: { appState, action in 35 | switch action { 36 | case .numberFacts(let action): 37 | numberFactsReducer.reduce(&appState.numberFactsState, action) 38 | } 39 | } 40 | ) 41 | 42 | 43 | typealias AppStore = Store 44 | 45 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Preview Content/Preivew Data/PreviewData+AppStores.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewData+AppStores.swift 3 | // NumberFacts 4 | // 5 | // Created by CypherPoet on 2/19/20. 6 | // ✌️ 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension PreviewData { 13 | 14 | enum AppStores { 15 | 16 | static let `default`: AppStore = { 17 | AppStore( 18 | initialState: AppState(), 19 | appReducer: appReducer 20 | ) 21 | }() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Preview Content/Preivew Data/PreviewData+NumberFacts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewData+NumberFacts.swift 3 | // NumberFacts 4 | // 5 | // Created by CypherPoet on 2/20/20. 6 | // ✌️ 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | 13 | extension PreviewData { 14 | 15 | enum NumberFacts { 16 | 17 | static let sample1: NumberFact = { 18 | let context = CurrentApp.coreDataManager.mainContext 19 | let numberFact = NumberFact(context: context) 20 | 21 | numberFact.number = 22 22 | numberFact.category = .math 23 | numberFact.text = "408 is the 8^{th} Pell number." 24 | numberFact.currentLanguage = .english 25 | numberFact.translationLanguage = .french 26 | numberFact.translatedText = nil 27 | numberFact.isFavorite = true 28 | 29 | return numberFact 30 | }() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Preview Content/Preivew Data/PreviewData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewData.swift 3 | // NumberFacts 4 | // 5 | // Created by CypherPoet on 2/19/20. 6 | // ✌️ 7 | // 8 | 9 | import Foundation 10 | 11 | enum PreviewData {} 12 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Reusables/Extensions/Collection+deleteManagedObjects.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collection+deleteManagedObjects.swift 3 | // NumberFacts 4 | // 5 | // Created by CypherPoet on 2/24/20. 6 | // ✌️ 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | extension Collection where 14 | Element: NSManagedObject, 15 | Index == Int 16 | { 17 | 18 | func delete(at indices: IndexSet) { 19 | indices.forEach { index in 20 | let element = self[index] 21 | 22 | guard let context = element.managedObjectContext else { preconditionFailure() } 23 | 24 | context.delete(element) 25 | } 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Reusables/Extensions/NumberFact+errorFact.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberFact+errorFact.swift 3 | // NumberFacts 4 | // 5 | // Created by CypherPoet on 2/21/20. 6 | // ✌️ 7 | // 8 | 9 | import Foundation 10 | import NumbersAPIService 11 | import Common 12 | 13 | 14 | extension NumberFact { 15 | static let errorFactPayload: NumbersAPIServicing.NumberFactPayload = ( 16 | text: "An error occurred while fetching number facts. Some software uses \"-1\" as a code to represent errors.", 17 | number: -1, 18 | category: .trivia 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Scenes/Favorite Number Facts/FavoriteNumberFactsListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteNumberFactsListView.swift 3 | // NumberFacts 4 | // 5 | // Created by CypherPoet on 2/23/20. 6 | // ✌️ 7 | // 8 | 9 | import SwiftUI 10 | import Common 11 | 12 | 13 | struct FavoriteNumberFactsListView { 14 | var numberFacts: [NumberFact] 15 | 16 | let onFactsDeleted: ((IndexSet) -> Void)? 17 | // let onLanguageToggled: ((Language) -> Void)? 18 | } 19 | 20 | 21 | // MARK: - View 22 | extension FavoriteNumberFactsListView: View { 23 | 24 | var body: some View { 25 | List { 26 | ForEach(numberFacts) { numberFact in 27 | Text(numberFact.text) 28 | } 29 | .onDelete(perform: onFactsDeleted) 30 | } 31 | .navigationBarTitle("Favorite Facts") 32 | } 33 | } 34 | 35 | 36 | // MARK: - Computeds 37 | extension FavoriteNumberFactsListView { 38 | } 39 | 40 | 41 | // MARK: - View Variables 42 | extension FavoriteNumberFactsListView { 43 | } 44 | 45 | 46 | // MARK: - Private Helpers 47 | private extension FavoriteNumberFactsListView { 48 | } 49 | 50 | 51 | 52 | // MARK: - Preview 53 | struct FavoriteNumberFactsListView_Previews: PreviewProvider { 54 | 55 | static var previews: some View { 56 | NavigationView { 57 | FavoriteNumberFactsListView( 58 | numberFacts: [ 59 | PreviewData.NumberFacts.sample1, 60 | ], 61 | onFactsDeleted: { _ in } 62 | // onLanguageToggled: { _ in } 63 | ) 64 | } 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Scenes/HomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeView.swift 3 | // NumberFacts 4 | // 5 | // Created by CypherPoet on 2/19/20. 6 | // ✌️ 7 | // 8 | 9 | import SwiftUI 10 | 11 | 12 | struct HomeView { 13 | enum Tab { 14 | case numberFactsFeed 15 | case favoriteNumberFacts 16 | } 17 | 18 | 19 | @State private var activeTab: Tab = .numberFactsFeed 20 | } 21 | 22 | 23 | // MARK: - View 24 | extension HomeView: View { 25 | 26 | var body: some View { 27 | TabView(selection: $activeTab) { 28 | NumberFactsFeedContainerView() 29 | .tabItem { 30 | Image(systemName: "number.circle.fill") 31 | Text("Feed") 32 | } 33 | .tag(Tab.numberFactsFeed) 34 | 35 | 36 | FavoriteNumberFactsContainerView() 37 | .tabItem { 38 | Image(systemName: "star.fill") 39 | Text("Favorites") 40 | } 41 | .tag(Tab.favoriteNumberFacts) 42 | } 43 | } 44 | } 45 | 46 | 47 | // MARK: - Computeds 48 | extension HomeView { 49 | } 50 | 51 | 52 | // MARK: - View Variables 53 | extension HomeView { 54 | } 55 | 56 | 57 | // MARK: - Private Helpers 58 | private extension HomeView { 59 | } 60 | 61 | 62 | 63 | // MARK: - Preview 64 | struct HomeView_Previews: PreviewProvider { 65 | 66 | static var previews: some View { 67 | HomeView() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFacts/Scenes/RootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootView.swift 3 | // NumberFacts 4 | // 5 | // Created by CypherPoet on 2/18/20. 6 | // ✌️ 7 | // 8 | 9 | import SwiftUI 10 | 11 | 12 | struct RootView { 13 | @Environment(\.managedObjectContext) private var managedObjectContext 14 | @EnvironmentObject var store: AppStore 15 | } 16 | 17 | 18 | // MARK: - View 19 | extension RootView: View { 20 | 21 | var body: some View { 22 | HomeView() 23 | } 24 | } 25 | 26 | 27 | // MARK: - Computeds 28 | extension RootView { 29 | } 30 | 31 | 32 | // MARK: - View Variables 33 | extension RootView { 34 | } 35 | 36 | 37 | // MARK: - Private Helpers 38 | private extension RootView { 39 | } 40 | 41 | 42 | 43 | // MARK: - Preview 44 | struct RootView_Previews: PreviewProvider { 45 | 46 | static var previews: some View { 47 | RootView() 48 | .environmentObject(PreviewData.AppStores.default) 49 | .environment(\.managedObjectContext, CurrentApp.coreDataManager.mainContext) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFactsTests/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 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumberFacts/NumberFactsTests/NumberFactsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberFactsTests.swift 3 | // NumberFactsTests 4 | // 5 | // Created by Brian Sipple on 2/14/20. 6 | // Copyright © 2020 CypherPoet. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NumberFacts 11 | 12 | class NumberFactsTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumbersAPIService/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumbersAPIService/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CypherPoetNetStack", 6 | "repositoryURL": "https://github.com/CypherPoet/CypherPoetNetStack.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "b5dbd717cd51726ccba1041ee5981680aaeff95f", 10 | "version": "0.0.28" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumbersAPIService/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "NumbersAPIService", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15), 11 | ], 12 | products: [ 13 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 14 | .library( 15 | name: "NumbersAPIService", 16 | targets: [ 17 | "NumbersAPIService", 18 | ] 19 | ), 20 | ], 21 | dependencies: [ 22 | // Dependencies declare other packages that this package depends on. 23 | .package(path: "../Common"), 24 | 25 | .package(url: "https://github.com/CypherPoet/CypherPoetNetStack.git", from: "0.0.28"), 26 | ], 27 | targets: [ 28 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 29 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 30 | .target( 31 | name: "NumbersAPIService", 32 | dependencies: [ 33 | "Common", 34 | "CypherPoetNetStack", 35 | ], 36 | path: "Sources/" 37 | ), 38 | 39 | .testTarget( 40 | name: "NumbersAPIServiceTests", 41 | dependencies: [ 42 | "NumbersAPIService", 43 | ], 44 | path: "Tests/NumbersAPIService" 45 | ), 46 | ] 47 | ) 48 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumbersAPIService/README.md: -------------------------------------------------------------------------------- 1 | # NumbersAPIService 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumbersAPIService/Sources/Extensions/Endpoint+NumbersAPI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CypherPoetNetStack_Core 3 | 4 | 5 | extension Endpoint { 6 | 7 | public enum NumbersAPI { 8 | private static let host = "numbersapi.com" 9 | 10 | 11 | public static var randomYearFact: Endpoint { 12 | .init( 13 | scheme: "http", 14 | host: host, 15 | path: "/random/year" 16 | ) 17 | } 18 | 19 | 20 | public static var randomDateFact: Endpoint { 21 | .init( 22 | scheme: "http", 23 | host: host, 24 | path: "/random/date" 25 | ) 26 | } 27 | 28 | 29 | public static var randomTriviaFact: Endpoint { 30 | .init( 31 | scheme: "http", 32 | host: host, 33 | path: "/random/trivia" 34 | ) 35 | } 36 | 37 | 38 | public static var randomMathFact: Endpoint { 39 | .init( 40 | scheme: "http", 41 | host: host, 42 | path: "/random/math" 43 | ) 44 | } 45 | } 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumbersAPIService/Sources/NumbersAPIService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CypherPoetNetStack 3 | 4 | 5 | public final class NumbersAPIService { 6 | public var session: URLSession 7 | public var apiQueue: DispatchQueue 8 | 9 | public init( 10 | session: URLSession = .shared, 11 | queue: DispatchQueue = DispatchQueue(label: "NumbersAPIService", qos: .userInitiated) 12 | ) { 13 | self.session = session 14 | self.apiQueue = queue 15 | } 16 | } 17 | 18 | 19 | extension NumbersAPIService: NumbersAPIServicing {} 20 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumbersAPIService/Sources/NumbersAPIServiceError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CypherPoetNetStack 3 | 4 | 5 | public enum NumbersAPIServiceError: LocalizedError { 6 | case network(error: NetStackError) 7 | case parsing(response: HTTPURLResponse, data: Data) 8 | case generic(error: Error) 9 | } 10 | 11 | 12 | extension NumbersAPIServiceError { 13 | 14 | public var errorDescription: String? { 15 | switch self { 16 | case .network(let error): 17 | return error.errorDescription 18 | case .parsing(let response, let data): 19 | return "Unable to make NumberFact from HTTPURLResponse and Data" 20 | case .generic: 21 | return "Unknown error type" 22 | } 23 | } 24 | } 25 | 26 | 27 | // MARK: - Error: Identifiable 28 | extension NumbersAPIServiceError: Identifiable { 29 | public var id: String? { errorDescription } 30 | } 31 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumbersAPIService/Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import NumbersAPIServiceTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += NumbersAPIServiceTests.__allTests() 7 | 8 | XCTMain(tests) 9 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/NumbersAPIService/Tests/NumbersAPIService/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension NumbersAPIServiceTests { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__NumbersAPIServiceTests = [ 9 | ("testFetchRandomYearFact", testFetchRandomYearFact), 10 | ] 11 | } 12 | 13 | public func __allTests() -> [XCTestCaseEntry] { 14 | return [ 15 | testCase(NumbersAPIServiceTests.__allTests__NumbersAPIServiceTests), 16 | ] 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Playgrounds/NumbersAPI.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PlaygroundSupport 3 | 4 | PlaygroundPage.current.needsIndefiniteExecution = true 5 | 6 | let url = URL(string: "http://numbersapi.com/random/math")! 7 | 8 | 9 | URLSession.shared.dataTask(with: url) { (data, response, error) in 10 | print(String(data: data!, encoding: .utf8)!) 11 | 12 | guard let response = response as? HTTPURLResponse else { return } 13 | 14 | print(response.value(forHTTPHeaderField: "X-Numbers-API-Number")) 15 | print(response.value(forHTTPHeaderField: "X-Numbers-API-Type")) 16 | 17 | PlaygroundPage.current.finishExecution() 18 | } 19 | .resume() 20 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/Playgrounds/NumbersAPI.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/TranslationService/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/TranslationService/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TranslationService", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15), 11 | ], 12 | products: [ 13 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 14 | .library( 15 | name: "TranslationService", 16 | targets: ["TranslationService"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | .package(path: "../Common"), 21 | 22 | .package(url: "https://github.com/CypherPoet/CypherPoetNetStack.git", from: "0.0.28"), 23 | ], 24 | targets: [ 25 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 26 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 27 | .target( 28 | name: "TranslationService", 29 | dependencies: [ 30 | "Common", 31 | "CypherPoetNetStack", 32 | ], 33 | path: "Sources/" 34 | ), 35 | 36 | 37 | .testTarget( 38 | name: "TranslationServiceTests", 39 | dependencies: [ 40 | "TranslationService" 41 | ], 42 | path: "Tests/TranslationService" 43 | ), 44 | ] 45 | ) 46 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/TranslationService/README.md: -------------------------------------------------------------------------------- 1 | # TranslationService 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/TranslationService/Sources/Extensions/Endpoint+TranslationAPI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Common 3 | import CypherPoetNetStack_Core 4 | 5 | 6 | extension Endpoint { 7 | 8 | public enum TranslationAPI { 9 | private static let apiKey = "trnsl.1.1.20190822T112140Z.d96fa7f4ed58ada0.f7a7297172fb385a6ae2c415b252b0d530e6f495" 10 | 11 | private static let scheme = "https" 12 | private static let host = "translate.yandex.net" 13 | private static let path = "/api/v1.5/tr.json/translate" 14 | 15 | 16 | public static func translation( 17 | for text: String, 18 | convertingFrom sourceLanguage: Language, 19 | to targetLanguage: Language 20 | ) -> Endpoint { 21 | .init( 22 | scheme: scheme, 23 | host: host, 24 | path: path, 25 | queryItems: [ 26 | URLQueryItem(name: "key", value: apiKey), 27 | URLQueryItem(name: "text", value: text), 28 | URLQueryItem( 29 | name: "lang", 30 | value: "\(sourceLanguage.code)-\(targetLanguage.code)" 31 | ), 32 | ] 33 | ) 34 | } 35 | } 36 | } 37 | 38 | 39 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/TranslationService/Sources/TranslationAPIService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CypherPoetNetStack 3 | 4 | 5 | public final class TranslationAPIService { 6 | public var session: URLSession 7 | public var apiQueue: DispatchQueue 8 | 9 | public init( 10 | session: URLSession = .shared, 11 | queue: DispatchQueue = DispatchQueue(label: "TranslationAPIService", qos: .userInitiated) 12 | ) { 13 | self.session = session 14 | self.apiQueue = queue 15 | } 16 | } 17 | 18 | 19 | extension TranslationAPIService: TranslationAPIServicing {} 20 | -------------------------------------------------------------------------------- /20-building-a-complete-app/NumberFacts/TranslationService/Sources/TranslationAPIServiceError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CypherPoetNetStack 3 | 4 | 5 | public enum TranslationAPIServiceError: Error { 6 | case network(error: NetStackError) 7 | } 8 | 9 | 10 | extension TranslationAPIServiceError { 11 | 12 | public var errorDescription: String? { 13 | switch self { 14 | case .network(let error): 15 | return error.errorDescription 16 | } 17 | } 18 | } 19 | 20 | // MARK: - Error: Identifiable 21 | extension TranslationAPIServiceError: Identifiable { 22 | public var id: String? { errorDescription } 23 | } 24 | -------------------------------------------------------------------------------- /Playgrounds.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Combine: Asynchronous Programming with Swift 2 | 3 | _Projects, playgrounds, and other materials made while following along with the Ray Wenderlich book ["Combine: Asynchronous Programming with Swift"](https://store.raywenderlich.com/products/combine-asynchronous-programming-with-swift)._ 4 | 5 | 6 | # Contents 7 | 8 | - [Chapter 2: Queues & Threads](./02-publishers-and-subscribers) 9 | - Creating publishers and subscribing to them. 10 | - Subjects 11 | - Dynamically Adjusting Demand 12 | - Type Erasure 13 | - **🥅 Challenge:** Create a Blackjack Card Dealer 14 | 15 | 16 | - [Chapter 8: Combine in Practice: Building a Photo Collage App](./08-photo-collage-app) 17 | - Using Combine publishers in custom views. 18 | - Handling user events with Combine. 19 | - Navigating between views and exchanging data via publishers. 20 | - Using a variety of operators to create different subscriptions to implement your app's logic. 21 | - Wrapping existing Cocoa APIs so you can conveniently use them in your Combine code. 22 | 23 | 24 | - Chapters 9-14: Networking with Combine 25 | 26 | 27 | - [Chapters 15-19: Bulding an App with SwiftUI and Combine Networking](./15-19-bitcoin-average-api-app) 28 | 29 | 30 | - [Chapter 16: Error Handling](./16-error-handling) 31 | 32 | 33 | - [Chapter 17: Schedulers](./17-schedulers) 34 | 35 | 36 | - [Chapter 18: Custom Publishers & Handling Backpressure](./18-custom-publishers-and-handling-backpressure) 37 | 38 | 39 | - [Chapter 19: Testing Combine Code](./19-testing-combine-code) 40 | 41 | 42 | - [Chapter 20: Building A Complete App with Combine, SwiftUI, and CoreData](./20-building-a-complete-app) 43 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Challenge.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | /// Challenge: Create a Blackjack card dealer 4 | 5 | import Foundation 6 | import Combine 7 | 8 | demo(describing: "Challenge: Create a Blackjack card dealer") { 9 | var subscriptions = Set() 10 | var dealersDeck = Deck.deckOf52() 11 | 12 | let dealtHand = PassthroughSubject() 13 | 14 | func deal(_ cardCount: Int) { 15 | var cardsRemaining = dealersDeck.cards.count 16 | var hand = Hand() 17 | 18 | for _ in 0 ..< cardCount { 19 | let randomIndex = Int.random(in: 0 ..< cardsRemaining) 20 | hand.append(dealersDeck.cards[randomIndex]) 21 | 22 | dealersDeck.cards.remove(at: randomIndex) 23 | cardsRemaining -= 1 24 | 25 | if hand.isBusted { 26 | dealtHand.send(completion: .failure(.busted)) 27 | } else { 28 | dealtHand.send(hand) 29 | } 30 | } 31 | } 32 | 33 | dealtHand 34 | .sink(receiveCompletion: { completion in 35 | switch completion { 36 | case .failure(let error): 37 | print(error) 38 | default: 39 | print("Hand finished") 40 | } 41 | }) { hand in 42 | print("Current hand: \(hand.cardString), Points: \(hand.points)") 43 | } 44 | .store(in: &subscriptions) 45 | 46 | 47 | deal(3) 48 | } 49 | 50 | //: [Next](@next) 51 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Challenge.xcplaygroundpage/Sources/Deck.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Deck { 4 | public var cards: [PlayingCard] 5 | 6 | 7 | public init(cards: [PlayingCard]) { 8 | self.cards = cards 9 | } 10 | 11 | 12 | public static func deckOf52() -> Deck { 13 | Deck(cards: makeDeckOf52Cards()) 14 | } 15 | } 16 | 17 | 18 | extension Deck { 19 | 20 | private static func makeDeckOf52Cards() -> [PlayingCard] { 21 | Suit.allCases.map { suit in 22 | Rank.allCases.map { rank in 23 | PlayingCard(suit: suit, rank: rank ) 24 | } 25 | } 26 | .flatMap { $0 } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Challenge.xcplaygroundpage/Sources/Hand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public typealias Hand = [PlayingCard] 5 | 6 | public extension Hand { 7 | 8 | var points: Int { 9 | reduce(0) { (accumulatedPoints, card) in 10 | accumulatedPoints + card.points 11 | } 12 | } 13 | 14 | var cardString: String { 15 | map { "\($0.rank)\($0.suit)" }.joined(separator: ", ") 16 | } 17 | 18 | var isBusted: Bool { points > 21 } 19 | } 20 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Challenge.xcplaygroundpage/Sources/HandError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public enum HandError: Error, CustomStringConvertible { 5 | case busted 6 | 7 | public var description: String { 8 | switch self { 9 | case .busted: 10 | return "Busted!" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Challenge.xcplaygroundpage/Sources/PlayingCard+Points.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | extension PlayingCard { 5 | 6 | public var points: Int { 7 | switch self.rank { 8 | case .jack, 9 | .queen, 10 | .king: 11 | return 10 12 | case .ace: 13 | return 11 14 | default: 15 | return self.rank.rawValue 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Challenge.xcplaygroundpage/Sources/PlayingCard.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct PlayingCard { 4 | public let suit: Suit 5 | public let rank: Rank 6 | 7 | 8 | public init(suit: Suit, rank: Rank) { 9 | self.suit = suit 10 | self.rank = rank 11 | } 12 | } 13 | 14 | extension PlayingCard: Equatable {} 15 | extension PlayingCard: Hashable {} 16 | 17 | 18 | extension PlayingCard: Comparable { 19 | public static func < (lhs: PlayingCard, rhs: PlayingCard) -> Bool { 20 | if lhs.rank == rhs.rank { 21 | // Suit is a tie-breaker when the rank matches 22 | return lhs.suit < rhs.suit 23 | } else { 24 | return lhs.rank < rhs.rank 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Challenge.xcplaygroundpage/Sources/Rank.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Rank: Int, CaseIterable { 4 | case two = 2 5 | case three 6 | case four 7 | case five 8 | case six 9 | case seven 10 | case eight 11 | case nine 12 | case ten 13 | 14 | case jack 15 | case queen 16 | case king 17 | case ace 18 | } 19 | 20 | 21 | 22 | // MARK: - Comparable 23 | extension Rank: Comparable { 24 | public static func < (lhs: Rank, rhs: Rank) -> Bool { 25 | switch (lhs, rhs) { 26 | case (_, _) where lhs == rhs: 27 | return false 28 | case (.ace, _): 29 | return false 30 | case (_, _): 31 | return lhs.rawValue < rhs.rawValue 32 | } 33 | } 34 | } 35 | 36 | 37 | 38 | // MARK: - CustomStringConvertible 39 | extension Rank: CustomStringConvertible { 40 | public var description: String { 41 | switch self { 42 | case .ace: return "A" 43 | case .jack: return "J" 44 | case .queen: return "Q" 45 | case .king: return "K" 46 | default: 47 | return "\(rawValue)" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Challenge.xcplaygroundpage/Sources/Suit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Suit: String, CaseIterable { 4 | case spades 5 | case hearts 6 | case diamonds 7 | case clubs 8 | } 9 | 10 | 11 | extension Suit: Comparable { 12 | public static func < (lhs: Suit, rhs: Suit) -> Bool { 13 | switch (lhs, rhs) { 14 | case (_, _) where lhs == rhs: 15 | return false 16 | case (.spades, _), 17 | (.hearts, .diamonds), 18 | (.hearts, .clubs), 19 | (.diamonds, .clubs): 20 | return false 21 | case (_, _): 22 | return true 23 | } 24 | } 25 | } 26 | 27 | 28 | extension Suit: CustomStringConvertible { 29 | 30 | public var description: String { 31 | switch self { 32 | case .spades: 33 | return "♠️" 34 | case .hearts: 35 | return "♥️" 36 | case .diamonds: 37 | return "♦️" 38 | case .clubs: 39 | return "♣️" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Dynamically Adjusting Demand.xcplaygroundpage/Sources/IntSubscriber.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | // 5 | //public final class IntSubscriber: Subscriber { 6 | // public typealias Input = Int 7 | // public typealias Failure = Never 8 | // 9 | // public init() {} 10 | //} 11 | // 12 | // 13 | //extension IntSubscriber { 14 | // 15 | // public func receive(subscription: Subscription) { 16 | // subscription.request(.max(2)) 17 | // } 18 | // 19 | // 20 | // public func receive(_ input: Input) -> Subscribers.Demand { 21 | // print("(Subscriber) Received Input: \(input)") 22 | // 23 | // switch input { 24 | // case 1: 25 | // return .max(2) 26 | // case 3: 27 | // return .max(1) 28 | // default: 29 | // return .none 30 | // } 31 | // } 32 | // 33 | // 34 | // public func receive(completion: Subscribers.Completion) { 35 | // print("(Subscriber) Received completion: \(completion)") 36 | // } 37 | // 38 | //} 39 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Publishers & Subscribers Intro.xcplaygroundpage/Sources/IntSubscriber.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | 4 | public final class IntSubscriber: Subscriber { 5 | public typealias Input = Int 6 | public typealias Failure = Never 7 | 8 | // var combineIdentifier: CombineIdentifier 9 | 10 | 11 | public init() {} 12 | 13 | 14 | public func receive(subscription: Subscription) { 15 | // subscription.request(.max(3)) 16 | subscription.request(.unlimited) 17 | } 18 | 19 | public func receive(_ input: Int) -> Subscribers.Demand { 20 | print("Subscriber received value: \(input)") 21 | 22 | // Tell the publisher that we aren't adjusting the demand after receiving 23 | return .none 24 | } 25 | 26 | public func receive(completion: Subscribers.Completion) { 27 | print("Subscriber received completion: \(completion)") 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Subjects.xcplaygroundpage/Sources/CustomError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum CustomError: Error { 4 | case oops 5 | } 6 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Subjects.xcplaygroundpage/Sources/StringSubscriber.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | public enum MyError: Error { 5 | case oops 6 | } 7 | 8 | 9 | public final class StringSubscriber: Subscriber { 10 | public typealias Input = String 11 | public typealias Failure = CustomError 12 | 13 | public init() {} 14 | } 15 | 16 | extension StringSubscriber { 17 | 18 | public func receive(subscription: Subscription) { 19 | subscription.request(.max(2)) 20 | } 21 | 22 | 23 | public func receive(_ input: String) -> Subscribers.Demand { 24 | print("String Subscriber -- Recevied input: \(input)") 25 | 26 | return input == "World" ? .max(1) : .none 27 | } 28 | 29 | 30 | public func receive(completion: Subscribers.Completion) { 31 | print("String Subscriber -- Received completion: \(completion)") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/02 - Type Erasure.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | 7 | demo(describing: "Type Erasure") { 8 | var subscriptions = Set() 9 | let subject = PassthroughSubject() 10 | let publisher = subject.eraseToAnyPublisher() 11 | 12 | 13 | publisher 14 | .sink(receiveValue: { print($0) }) 15 | .store(in: &subscriptions) 16 | 17 | subject.send("⚡️") 18 | subject.send("🦄") 19 | 20 | } 21 | 22 | //: [Next](@next) 23 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/03 - Challenge.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | //: 🥅 Challenge: Create a phone number lookup using transforming operators 4 | 5 | import Foundation 6 | import Combine 7 | 8 | 9 | var subscribers = Set() 10 | 11 | 12 | demo( 13 | describing: "Challenge: Creating a phone number lookup using transforming operators" 14 | ) { 15 | let inputReceiver = PassthroughSubject() 16 | 17 | inputReceiver 18 | .map(Phone.numberFromInput) 19 | .replaceNil(with: 0) 20 | .collect(10) 21 | .map { digits in 22 | digits.reduce("", { (digitString, currentDigit) in 23 | "\(digitString)\(currentDigit)" 24 | }) 25 | } 26 | .map(PhoneBook.formattedPhoneNumber(from:)) 27 | .print() 28 | .map(PhoneBook.dial(phoneNumber:)) 29 | .sink( 30 | receiveCompletion: { completion in 31 | print("(sink) Received completion: \(completion)") 32 | }, 33 | receiveValue: { value in 34 | print("(sink) Received value: \(value)") 35 | } 36 | ) 37 | .store(in: &subscribers) 38 | 39 | 40 | "0123456789".forEach { inputReceiver.send($0) } 41 | "7777777777".forEach { inputReceiver.send($0) } 42 | "✌️🙂🍁🦅3🙂🍁🦅✌️🙂".forEach { inputReceiver.send($0) } 43 | } 44 | 45 | 46 | 47 | //: [Next](@next) 48 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/03 - Challenge.xcplaygroundpage/Sources/Phone.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Phone { 4 | 5 | private static let keypad = [ 6 | "abc": 1, 7 | "def": 2, 8 | "ghi": 3, 9 | "jkl": 4, 10 | "mno": 5, 11 | "pqr": 6, 12 | "stu": 7, 13 | "vqr": 8, 14 | "wxyz": 9 15 | ] 16 | 17 | 18 | public static func numberFromInput(_ input: Character) -> Int? { 19 | if let number = Int(String(input)), number < 10 { 20 | return number 21 | } 22 | 23 | for letters in Self.keypad.keys { 24 | if letters.contains(input) { 25 | return Self.keypad[letters] 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/03 - Collecting Values.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | 7 | var cancellables = Set() 8 | 9 | 10 | demo(describing: "The `collect` operator") { 11 | let elements = ["🌍", "💨", "🔥", "💦", "🧙‍♂️"] 12 | 13 | elements 14 | .publisher 15 | .collect() 16 | .sink( 17 | receiveCompletion: { print($0) }, 18 | receiveValue: { print($0) } 19 | ) 20 | .store(in: &cancellables) 21 | 22 | 23 | elements 24 | .publisher 25 | .collect(2) 26 | .sink( 27 | receiveCompletion: { print($0) }, 28 | receiveValue: { print($0) } 29 | ) 30 | .store(in: &cancellables) 31 | /// The last value is still emitted as an array. That’s because the upstream publisher completed 32 | /// before collect filled its prescribed buffer, so it sent whatever it had left as an array. 33 | } 34 | 35 | 36 | //: [Next](@next) 37 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/03 - Flattening Publishers.xcplaygroundpage/Sources/Chatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | 5 | public struct Chatter { 6 | public let name: String 7 | public let message: CurrentValueSubject 8 | 9 | 10 | public init(name: String, message: String) { 11 | self.name = name 12 | self.message = CurrentValueSubject(message) 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/03 - Incrementally Transforming Output.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | 7 | var subscriptions = Set() 8 | 9 | 10 | 11 | //: `scan` will provide the current value emitted by an upstream 12 | //: publisher to a closure, along with the last value returned by that closure 13 | 14 | 15 | demo(describing: "The `scan` publisher") { 16 | let dailyPriceChanges = (0...30).map { _ in Double.random(in: -100...100) } 17 | 18 | dailyPriceChanges.publisher 19 | .scan(0) { (previousResult, currentValue) in 20 | max(0, previousResult + currentValue) 21 | } 22 | .sink(receiveValue: { _ in }) 23 | .store(in: &subscriptions) 24 | } 25 | 26 | 27 | //: [Next](@next) 28 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/03 - Mapping Keypaths.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | 7 | var subscriptions = Set() 8 | 9 | 10 | demo(describing: "Mapping multiple keypaths") { 11 | let publisher = PassthroughSubject() 12 | 13 | publisher 14 | .map(\.x, \.y) 15 | .sink { (x, y) in 16 | print(""" 17 | The coordinate at (\(x), \(y)) is \(Coordinate.quadrantDescriptionOf(x: x, y: y)) 18 | """ 19 | ) 20 | } 21 | .store(in: &subscriptions) 22 | 23 | publisher.send(Coordinate(x: 9, y: 10)) 24 | publisher.send(Coordinate(x: 0, y: 10)) 25 | publisher.send(Coordinate(x: -1, y: 10)) 26 | publisher.send(Coordinate(x: 0, y: 0)) 27 | publisher.send(Coordinate(x: 10, y: 0)) 28 | } 29 | 30 | //: [Next](@next) 31 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/03 - Mapping Keypaths.xcplaygroundpage/Sources/Coordinate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | 5 | public struct Coordinate { 6 | public var x: CGFloat 7 | public var y: CGFloat 8 | 9 | 10 | public init(x: CGFloat = 0, y: CGFloat = 0) { 11 | self.x = x 12 | self.y = y 13 | } 14 | } 15 | 16 | 17 | extension Coordinate { 18 | 19 | public var quadrant: String { 20 | Self.quadrantDescriptionOf(x: x, y: y) 21 | } 22 | 23 | 24 | public static func quadrantDescriptionOf(x: CGFloat, y: CGFloat) -> String { 25 | switch (x, y) { 26 | case (0, 0): 27 | return "on the Origin" 28 | case let (x, _) where x == 0: 29 | return "on the Y-Axis" 30 | case let (_, y) where y == 0: 31 | return "on the X-Axis" 32 | case let (x, y) where x > 0: 33 | return "in Quadrant \(y > 0 ? "1" : "4")" 34 | case let (x, y) where x < 0: 35 | return "in Quadrant \(y > 0 ? "3" : "3")" 36 | default: 37 | fatalError() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/03 - Mapping Values.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | //: In addition to collecting values, you’ll often want to transform those values in some way. 4 | //: Combine offers several mapping operators for that purpose 5 | 6 | import Foundation 7 | import Combine 8 | 9 | var cancellables = Set() 10 | 11 | 12 | demo(describing: "The `map` operator") { 13 | let numbers = [1, 1, 2, 3, 5, 8, 13, 21, 34] 14 | let formatter = NumberFormatter() 15 | 16 | formatter.numberStyle = .spellOut 17 | 18 | numbers 19 | .publisher 20 | .map({ formatter.string(from: $0 as NSNumber) ?? "" }) 21 | .sink( 22 | receiveCompletion: { print($0) }, 23 | receiveValue: { print($0) } 24 | ) 25 | .store(in: &cancellables) 26 | } 27 | 28 | 29 | //: [Next](@next) 30 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/03 - Replacing Upstream Output.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | 7 | var subscriptions = Set() 8 | 9 | 10 | demo(describing: "`replaceNil`") { 11 | ["⚡️", "⚡️", nil] 12 | .publisher 13 | .replaceNil(with: "💥") 14 | .sink { print($0) } 15 | .store(in: &subscriptions) 16 | } 17 | 18 | 19 | demo(describing: "`replaceNil` and unwrap") { 20 | ["⚡️", "⚡️", nil] 21 | .publisher 22 | .replaceNil(with: "💥") 23 | .map({ $0! }) 24 | .sink { print($0) } 25 | .store(in: &subscriptions) 26 | } 27 | 28 | 29 | demo(describing: "`replaceEmpty`") { 30 | [].publisher 31 | .replaceEmpty(with: "🦕") 32 | .sink( 33 | receiveCompletion: { completion in 34 | print(completion) 35 | }, 36 | receiveValue: { value in 37 | print(value) 38 | } 39 | ) 40 | .store(in: &subscriptions) 41 | 42 | 43 | let empty = Empty() 44 | 45 | empty 46 | .replaceEmpty(with: 42) 47 | .sink( 48 | receiveCompletion: { completion in print(completion) }, 49 | receiveValue: { value in print(value) } 50 | ) 51 | .store(in: &subscriptions) 52 | } 53 | 54 | //: [Next](@next) 55 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/03 - TryMap.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | //: Several operators, including map, have a counterpart try operator that 4 | //: will take a closure that can throw an error. 5 | //: 6 | //: If you throw an error, it will emit that error downstream. 7 | 8 | import Foundation 9 | import Combine 10 | 11 | 12 | var subscriptions = Set() 13 | 14 | 15 | demo(describing: "tryMap(_:)") { 16 | Just("Directory name that does not exist") 17 | .tryMap { try FileManager.default.contentsOfDirectory(atPath: $0) } 18 | .sink( 19 | receiveCompletion: { completion in 20 | switch completion { 21 | case .failure(let error): 22 | print("(sink) Completed with error: \(error.localizedDescription)") 23 | case .finished: 24 | print("(sink) Successful completion") 25 | } 26 | }, 27 | receiveValue: { value in 28 | print("(sink) Received value: \(value)") 29 | } 30 | ) 31 | .store(in: &subscriptions) 32 | } 33 | 34 | //: [Next](@next) 35 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/04 - Compacting and Ignoring.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | var subscriptions = Set() 7 | 8 | 9 | demo(describing: "The `compactMap` operator") { 10 | let charges = ["12.23", "Free", "N/A", "823", "3391", "-10"] 11 | 12 | charges 13 | .publisher 14 | .compactMap(Double.init) 15 | .sink { print($0) } 16 | .store(in: &subscriptions) 17 | } 18 | 19 | 20 | demo(describing: "The `ignoreOutput` operator") { 21 | let numbers = 1...10_000 22 | 23 | numbers 24 | .publisher 25 | .ignoreOutput() 26 | .sink( 27 | receiveCompletion: { completion in 28 | print("(sink) Received completion: \(completion)") 29 | }, 30 | receiveValue: { value in 31 | print("(sink) Received value: \(value)") 32 | } 33 | ) 34 | .store(in: &subscriptions) 35 | } 36 | 37 | //: [Next](@next) 38 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/04 - Dropping Values.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | 4 | import Foundation 5 | import Combine 6 | 7 | var subscriptions = Set() 8 | 9 | 10 | demo(describing: "The `dropFirst` operator") { 11 | let numbers = (1...10) 12 | 13 | numbers 14 | .publisher 15 | .dropFirst(4) 16 | .sink(receiveValue: { print($0) }) 17 | .store(in: &subscriptions) 18 | } 19 | 20 | 21 | demo(describing: "The `drop(while:)` operator") { 22 | let numbers = (1...10) 23 | 24 | numbers 25 | .publisher 26 | .drop(while: { number in 27 | print("Evaluating drop(while:) predicate") 28 | 29 | return number % 4 != 0 30 | }) 31 | .sink(receiveValue: { print($0) }) 32 | .store(in: &subscriptions) 33 | } 34 | 35 | 36 | demo(describing: "The `drop(untilOutputFrom:)` operator") { 37 | let readyFlag = PassthroughSubject() 38 | let numberPublisher = PassthroughSubject() 39 | 40 | numberPublisher 41 | .drop(untilOutputFrom: readyFlag) 42 | .sink(receiveValue: { print($0) }) 43 | .store(in: &subscriptions) 44 | 45 | 46 | numberPublisher.send(1) 47 | numberPublisher.send(1) 48 | numberPublisher.send(2) 49 | numberPublisher.send(3) 50 | numberPublisher.send(5) 51 | 52 | readyFlag.send() 53 | 54 | numberPublisher.send(8) 55 | } 56 | 57 | 58 | //: [Next](@next) 59 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/04 - Filtering Operators Intro.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | var subscriptions = Set() 7 | 8 | 9 | 10 | demo(describing: "The `filter` operator") { 11 | let numbers = (1...20).map { _ in Int.random(in: 1...100) } 12 | 13 | numbers.publisher 14 | .filter({ $0.isMultiple(of: 2) }) 15 | .sink { value in 16 | print("\(value) is even") 17 | } 18 | .store(in: &subscriptions) 19 | } 20 | 21 | 22 | 23 | demo(describing: "The `removeDuplicates` operator") { 24 | let sentence = "Keep it it secret! Keep Keep it safe!" 25 | 26 | sentence 27 | .components(separatedBy: " ") 28 | .publisher 29 | .removeDuplicates() 30 | .sink { print("(sink) Received value: \($0)") } 31 | .store(in: &subscriptions) 32 | } 33 | 34 | //: [Next](@next) 35 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/04- Challenge.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | //: 🥅 Challenge: Filter all the things 4 | //: 5 | //: Create an example that publishes a collection of numbers from 1 through 100, 6 | //: and use filtering operators to: 7 | //: 8 | //: - Skip the first 50 values emitted by the upstream publisher. 9 | //: - Take the next 20 values after those first 50 values. 10 | //: - Only take even numbers. 11 | //: 12 | //: The output of your example should produce the following numbers, one per line: 13 | //: 14 | //: 52 54 56 58 60 62 64 66 68 70 15 | //: 16 | 17 | 18 | import Foundation 19 | import Combine 20 | 21 | 22 | var subscriptions = Set() 23 | 24 | 25 | demo(describing: "Challenge for Chapter 4") { 26 | let numberPublisher = PassthroughSubject() 27 | 28 | numberPublisher 29 | .dropFirst(50) 30 | .prefix(20) 31 | .filter({ $0.isMultiple(of: 2) }) 32 | .sink( 33 | receiveCompletion: { completion in 34 | print("(sink) Received completion: \(completion)") 35 | }, 36 | receiveValue: { value in 37 | print("(sink) Received value: \(value)") 38 | } 39 | ) 40 | .store(in: &subscriptions) 41 | 42 | 43 | for number in (1...100) { 44 | numberPublisher.send(number) 45 | } 46 | } 47 | 48 | //: [Next](@next) 49 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/05 - Merge.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | 5 | import Combine 6 | 7 | 8 | var subscriptions = Set() 9 | 10 | demo(describing: "The `merge` publisher") { 11 | let numberStream1 = PassthroughSubject() 12 | let numberStream2 = PassthroughSubject() 13 | 14 | 15 | numberStream1 16 | .merge(with: numberStream2) 17 | .sink( 18 | receiveCompletion: { completion in 19 | print("(sink) Received Completion: \(completion)") 20 | }, 21 | receiveValue: { value in 22 | print("(sink) Received Value: \(value)") 23 | } 24 | ) 25 | .store(in: &subscriptions) 26 | 27 | 28 | numberStream1.send(111) 29 | numberStream1.send(111) 30 | numberStream2.send(222) 31 | numberStream2.send(222) 32 | numberStream1.send(111) 33 | numberStream2.send(222) 34 | 35 | numberStream1.send(completion: .finished) 36 | numberStream2.send(completion: .finished) 37 | } 38 | 39 | 40 | //: [Next](@next) 41 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/05 - zip.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | 7 | var subscriptions = Set() 8 | 9 | 10 | demo(describing: "The `zip` operator") { 11 | let numberStream = PassthroughSubject() 12 | let stringStream = PassthroughSubject() 13 | 14 | numberStream 15 | .zip(stringStream) 16 | .sink( 17 | receiveCompletion: { (completion) in 18 | print("(sink) Received Completion: \(completion)") 19 | }, 20 | receiveValue: { (number, string) in 21 | print("(sink) Receive Value -- number: \(number)") 22 | print("(sink) Receive Value -- string: \(string)") 23 | } 24 | ) 25 | .store(in: &subscriptions) 26 | 27 | 28 | [1, 2, 3].forEach { numberStream.send($0) } 29 | 30 | 31 | stringStream.send("⚡️") 32 | stringStream.send("🦄") 33 | stringStream.send("🤐") 34 | stringStream.send("🍁") 35 | 36 | 37 | numberStream.send(completion: .finished) 38 | stringStream.send(completion: .finished) 39 | } 40 | 41 | //: [Next](@next) 42 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Pages/05- combineLatest.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | //: `combineLatest` is another operator that lets you combine different publishers. 7 | //: It also lets you combine publishers of different 8 | //: value types, which can be extremely useful. 9 | 10 | 11 | var subscriptions = Set() 12 | 13 | 14 | demo(describing: "The `combineLatest` operator") { 15 | let numberStream = PassthroughSubject() 16 | let stringStream = PassthroughSubject() 17 | 18 | numberStream 19 | .combineLatest(stringStream) 20 | .sink(receiveCompletion: { (completion) in 21 | print("(sink) Received Completion: \(completion)") 22 | }, receiveValue: { (number, string) in 23 | print("(sink) Receive Value -- number: \(number)") 24 | print("(sink) Receive Value -- string: \(string)") 25 | }) 26 | .store(in: &subscriptions) 27 | 28 | 29 | numberStream.send(1) 30 | numberStream.send(2) 31 | numberStream.send(3) 32 | 33 | stringStream.send("Foo") 34 | stringStream.send("Bar") 35 | stringStream.send("Baz") 36 | stringStream.send("Wha?") 37 | numberStream.send(7) 38 | stringStream.send("Whoa!") 39 | 40 | numberStream.send(completion: .finished) 41 | stringStream.send(completion: .finished) 42 | } 43 | 44 | 45 | 46 | //: [Next](@next) 47 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.playground/Sources/PlaygroundUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public func demo(describing description: String, action: () -> Void) { 5 | print("\n--- Example of: \(description) ---") 6 | action() 7 | } 8 | -------------------------------------------------------------------------------- /chapters-2-5/Playgrounds/Chapters 2-5.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 | --------------------------------------------------------------------------------