├── .gitattributes ├── 01-introduction ├── fizz_buzz.swift ├── hello_world.swift └── test_fizz_buzz.swift ├── 04-tdd-in-the-real-world ├── 0-start │ ├── .gitignore │ ├── Albertos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Albertos.xcscheme │ ├── Albertos │ │ ├── AlbertosApp.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── Info.plist │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── AlbertosTests │ │ ├── AlbertosTests.swift │ │ └── Info.plist │ ├── AlbertosUITests │ │ ├── AlbertosUITests.swift │ │ └── Info.plist │ └── BuildPhases │ │ └── xcsort └── 1-end │ ├── .gitignore │ ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme │ ├── Albertos │ ├── AlbertosApp.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuList.swift │ ├── MenuSection.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ └── MenuGroupingTests.swift │ ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist │ └── BuildPhases │ └── xcsort ├── 05-fixtures ├── 0-start │ ├── .gitignore │ ├── Albertos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Albertos.xcscheme │ ├── Albertos │ │ ├── AlbertosApp.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── MenuGrouping.swift │ │ ├── MenuItem.swift │ │ ├── MenuList.swift │ │ ├── MenuSection.swift │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── AlbertosTests │ │ ├── Collection+Safe.swift │ │ ├── Info.plist │ │ └── MenuGroupingTests.swift │ ├── AlbertosUITests │ │ ├── AlbertosUITests.swift │ │ └── Info.plist │ └── BuildPhases │ │ └── xcsort └── 1-end │ ├── .gitignore │ ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme │ ├── Albertos │ ├── AlbertosApp.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuList.swift │ ├── MenuSection.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ └── MenuSection+Fixture.swift │ ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist │ └── BuildPhases │ └── xcsort ├── 06-testing-static-swiftui-views ├── 0-start │ ├── .gitignore │ ├── Albertos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Albertos.xcscheme │ ├── Albertos │ │ ├── AlbertosApp.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── MenuGrouping.swift │ │ ├── MenuItem.swift │ │ ├── MenuList.swift │ │ ├── MenuSection.swift │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── AlbertosTests │ │ ├── Collection+Safe.swift │ │ ├── Info.plist │ │ ├── MenuGroupingTests.swift │ │ ├── MenuItem+Fixture.swift │ │ └── MenuSection+Fixture.swift │ ├── AlbertosUITests │ │ ├── AlbertosUITests.swift │ │ └── Info.plist │ └── BuildPhases │ │ └── xcsort └── 1-end │ ├── .gitignore │ ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme │ ├── Albertos │ ├── AlbertosApp.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuList.ViewModel.swift │ ├── MenuList.swift │ ├── MenuRow.ViewModel.swift │ ├── MenuRow.swift │ ├── MenuSection.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuRow.ViewModelTests.swift │ └── MenuSection+Fixture.swift │ ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist │ └── BuildPhases │ └── xcsort ├── 07-testing-dynamic-swiftui-views ├── 0-start │ ├── .gitignore │ ├── Albertos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Albertos.xcscheme │ ├── Albertos │ │ ├── AlbertosApp.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── MenuGrouping.swift │ │ ├── MenuItem.swift │ │ ├── MenuList.ViewModel.swift │ │ ├── MenuList.swift │ │ ├── MenuRow.ViewModel.swift │ │ ├── MenuRow.swift │ │ ├── MenuSection.swift │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── AlbertosTests │ │ ├── Collection+Safe.swift │ │ ├── Info.plist │ │ ├── MenuGroupingTests.swift │ │ ├── MenuItem+Fixture.swift │ │ ├── MenuList.ViewModelTests.swift │ │ ├── MenuRow.ViewModelTests.swift │ │ └── MenuSection+Fixture.swift │ ├── AlbertosUITests │ │ ├── AlbertosUITests.swift │ │ └── Info.plist │ └── BuildPhases │ │ └── xcsort └── 1-end │ ├── .gitignore │ ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme │ ├── Albertos │ ├── AlbertosApp.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ ├── MenuFetching.swift │ ├── MenuFetchingPlaceholder.swift │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuList.ViewModel.swift │ ├── MenuList.swift │ ├── MenuRow.ViewModel.swift │ ├── MenuRow.swift │ ├── MenuSection.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuRow.ViewModelTests.swift │ └── MenuSection+Fixture.swift │ ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist │ └── BuildPhases │ └── xcsort ├── 08-stub ├── 0-start │ ├── .gitignore │ ├── Albertos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Albertos.xcscheme │ ├── Albertos │ │ ├── AlbertosApp.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── MenuFetching.swift │ │ ├── MenuFetchingPlaceholder.swift │ │ ├── MenuGrouping.swift │ │ ├── MenuItem.swift │ │ ├── MenuList.ViewModel.swift │ │ ├── MenuList.swift │ │ ├── MenuRow.ViewModel.swift │ │ ├── MenuRow.swift │ │ ├── MenuSection.swift │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── AlbertosTests │ │ ├── Collection+Safe.swift │ │ ├── Info.plist │ │ ├── MenuGroupingTests.swift │ │ ├── MenuItem+Fixture.swift │ │ ├── MenuList.ViewModelTests.swift │ │ ├── MenuRow.ViewModelTests.swift │ │ └── MenuSection+Fixture.swift │ ├── AlbertosUITests │ │ ├── AlbertosUITests.swift │ │ └── Info.plist │ └── BuildPhases │ │ └── xcsort └── 1-end │ ├── .gitignore │ ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme │ ├── Albertos │ ├── AlbertosApp.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ ├── MenuFetching.swift │ ├── MenuFetchingPlaceholder.swift │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuList.ViewModel.swift │ ├── MenuList.swift │ ├── MenuRow.ViewModel.swift │ ├── MenuRow.swift │ ├── MenuSection.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuFetchingStub.swift │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuRow.ViewModelTests.swift │ ├── MenuSection+Fixture.swift │ └── TestError.swift │ ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist │ └── BuildPhases │ └── xcsort ├── 09-json-decoding ├── 0-start │ ├── .gitignore │ ├── Albertos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Albertos.xcscheme │ ├── Albertos │ │ ├── AlbertosApp.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── MenuFetching.swift │ │ ├── MenuFetchingPlaceholder.swift │ │ ├── MenuGrouping.swift │ │ ├── MenuItem.swift │ │ ├── MenuList.ViewModel.swift │ │ ├── MenuList.swift │ │ ├── MenuRow.ViewModel.swift │ │ ├── MenuRow.swift │ │ ├── MenuSection.swift │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── AlbertosTests │ │ ├── Collection+Safe.swift │ │ ├── Info.plist │ │ ├── MenuFetchingStub.swift │ │ ├── MenuGroupingTests.swift │ │ ├── MenuItem+Fixture.swift │ │ ├── MenuList.ViewModelTests.swift │ │ ├── MenuRow.ViewModelTests.swift │ │ ├── MenuSection+Fixture.swift │ │ └── TestError.swift │ ├── AlbertosUITests │ │ ├── AlbertosUITests.swift │ │ └── Info.plist │ └── BuildPhases │ │ └── xcsort └── 1-end │ ├── .gitignore │ ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme │ ├── Albertos │ ├── AlbertosApp.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ ├── MenuFetching.swift │ ├── MenuFetchingPlaceholder.swift │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuList.ViewModel.swift │ ├── MenuList.swift │ ├── MenuRow.ViewModel.swift │ ├── MenuRow.swift │ ├── MenuSection.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuFetchingStub.swift │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuItem+JSONFixture.swift │ ├── MenuItemAlternateJSONTests.swift │ ├── MenuItemTests.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuRow.ViewModelTests.swift │ ├── MenuSection+Fixture.swift │ ├── TestError.swift │ ├── XCTestCase+JSON.swift │ └── menu_item.json │ ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist │ └── BuildPhases │ └── xcsort ├── 10-networking ├── 0-start │ ├── .gitignore │ ├── Albertos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Albertos.xcscheme │ ├── Albertos │ │ ├── AlbertosApp.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── MenuFetching.swift │ │ ├── MenuFetchingPlaceholder.swift │ │ ├── MenuGrouping.swift │ │ ├── MenuItem.swift │ │ ├── MenuList.ViewModel.swift │ │ ├── MenuList.swift │ │ ├── MenuRow.ViewModel.swift │ │ ├── MenuRow.swift │ │ ├── MenuSection.swift │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── AlbertosTests │ │ ├── Collection+Safe.swift │ │ ├── Info.plist │ │ ├── MenuFetchingStub.swift │ │ ├── MenuGroupingTests.swift │ │ ├── MenuItem+Fixture.swift │ │ ├── MenuItem+JSONFixture.swift │ │ ├── MenuItemAlternateJSONTests.swift │ │ ├── MenuItemTests.swift │ │ ├── MenuList.ViewModelTests.swift │ │ ├── MenuRow.ViewModelTests.swift │ │ ├── MenuSection+Fixture.swift │ │ ├── TestError.swift │ │ ├── XCTestCase+JSON.swift │ │ └── menu_item.json │ ├── AlbertosUITests │ │ ├── AlbertosUITests.swift │ │ └── Info.plist │ └── BuildPhases │ │ └── xcsort └── 1-end │ ├── .gitignore │ ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme │ ├── Albertos │ ├── AlbertosApp.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ ├── MenuFetcher.swift │ ├── MenuFetching.swift │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuList.ViewModel.swift │ ├── MenuList.swift │ ├── MenuRow.ViewModel.swift │ ├── MenuRow.swift │ ├── MenuSection.swift │ ├── NetworkFetching.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── URLSession+NetworkFetching.swift │ ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuFetcherTests.swift │ ├── MenuFetchingStub.swift │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuItem+JSONFixture.swift │ ├── MenuItemAlternateJSONTests.swift │ ├── MenuItemTests.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuRow.ViewModelTests.swift │ ├── MenuSection+Fixture.swift │ ├── NetworkFetchingStub.swift │ ├── TestError.swift │ ├── XCTestCase+JSON.swift │ └── menu_item.json │ ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist │ └── BuildPhases │ └── xcsort ├── 11-dependency-injection-with-environment-object ├── 0-start │ ├── .gitignore │ ├── Albertos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Albertos.xcscheme │ ├── Albertos │ │ ├── AlbertosApp.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Color+Custom.swift │ │ ├── Info.plist │ │ ├── MenuFetcher.swift │ │ ├── MenuFetching.swift │ │ ├── MenuGrouping.swift │ │ ├── MenuItem.swift │ │ ├── MenuItemDetail.ViewModel.swift │ │ ├── MenuItemDetail.swift │ │ ├── MenuList.ViewModel.swift │ │ ├── MenuList.swift │ │ ├── MenuRow.ViewModel.swift │ │ ├── MenuRow.swift │ │ ├── MenuSection.swift │ │ ├── NetworkFetching.swift │ │ ├── Order.swift │ │ ├── OrderButton.ViewModel.swift │ │ ├── OrderButton.swift │ │ ├── OrderController.swift │ │ ├── OrderDetail.ViewModel.swift │ │ ├── OrderDetail.swift │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ └── URLSession+NetworkFetching.swift │ ├── AlbertosTests │ │ ├── Collection+Safe.swift │ │ ├── Info.plist │ │ ├── MenuFetcherTests.swift │ │ ├── MenuFetchingStub.swift │ │ ├── MenuGroupingTests.swift │ │ ├── MenuItem+Fixture.swift │ │ ├── MenuItem+JSONFixture.swift │ │ ├── MenuItemAlternateJSONTests.swift │ │ ├── MenuItemDetail.ViewModelTests.swift │ │ ├── MenuItemTests.swift │ │ ├── MenuList.ViewModelTests.swift │ │ ├── MenuRow.ViewModelTests.swift │ │ ├── MenuSection+Fixture.swift │ │ ├── NetworkFetchingStub.swift │ │ ├── OrderButtonViewModelTests.swift │ │ ├── OrderControllerTests.swift │ │ ├── OrderDetail.ViewModelTests.swift │ │ ├── OrderTests.swift │ │ ├── TestError.swift │ │ ├── XCTestCase+JSON.swift │ │ └── menu_item.json │ ├── AlbertosUITests │ │ ├── AlbertosUITests.swift │ │ └── Info.plist │ └── BuildPhases │ │ └── xcsort └── 1-end │ ├── .gitignore │ ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme │ ├── Albertos │ ├── AlbertosApp.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Color+Custom.swift │ ├── Info.plist │ ├── MenuFetcher.swift │ ├── MenuFetching.swift │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuItemDetail.ViewModel.swift │ ├── MenuItemDetail.swift │ ├── MenuList.ViewModel.swift │ ├── MenuList.swift │ ├── MenuRow.ViewModel.swift │ ├── MenuRow.swift │ ├── MenuSection.swift │ ├── NetworkFetching.swift │ ├── Order.swift │ ├── OrderButton.ViewModel.swift │ ├── OrderButton.swift │ ├── OrderController.swift │ ├── OrderDetail.ViewModel.swift │ ├── OrderDetail.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── URLSession+NetworkFetching.swift │ ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuFetcherTests.swift │ ├── MenuFetchingStub.swift │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuItem+JSONFixture.swift │ ├── MenuItemAlternateJSONTests.swift │ ├── MenuItemDetail.ViewModelTests.swift │ ├── MenuItemTests.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuRow.ViewModelTests.swift │ ├── MenuSection+Fixture.swift │ ├── NetworkFetchingStub.swift │ ├── OrderButtonViewModelTests.swift │ ├── OrderControllerTests.swift │ ├── OrderDetail.ViewModelTests.swift │ ├── OrderTests.swift │ ├── TestError.swift │ ├── XCTestCase+JSON.swift │ └── menu_item.json │ ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist │ └── BuildPhases │ └── xcsort ├── 12-spy ├── 0-start │ ├── .gitignore │ ├── Albertos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Albertos.xcscheme │ ├── Albertos │ │ ├── AlbertosApp.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Color+Custom.swift │ │ ├── Info.plist │ │ ├── MenuFetcher.swift │ │ ├── MenuFetching.swift │ │ ├── MenuGrouping.swift │ │ ├── MenuItem.swift │ │ ├── MenuItemDetail.ViewModel.swift │ │ ├── MenuItemDetail.swift │ │ ├── MenuList.ViewModel.swift │ │ ├── MenuList.swift │ │ ├── MenuRow.ViewModel.swift │ │ ├── MenuRow.swift │ │ ├── MenuSection.swift │ │ ├── NetworkFetching.swift │ │ ├── Order.swift │ │ ├── OrderButton.ViewModel.swift │ │ ├── OrderButton.swift │ │ ├── OrderController.swift │ │ ├── OrderDetail.ViewModel.swift │ │ ├── OrderDetail.swift │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ └── URLSession+NetworkFetching.swift │ ├── AlbertosTests │ │ ├── Collection+Safe.swift │ │ ├── Info.plist │ │ ├── MenuFetcherTests.swift │ │ ├── MenuFetchingStub.swift │ │ ├── MenuGroupingTests.swift │ │ ├── MenuItem+Fixture.swift │ │ ├── MenuItem+JSONFixture.swift │ │ ├── MenuItemAlternateJSONTests.swift │ │ ├── MenuItemDetail.ViewModelTests.swift │ │ ├── MenuItemTests.swift │ │ ├── MenuList.ViewModelTests.swift │ │ ├── MenuRow.ViewModelTests.swift │ │ ├── MenuSection+Fixture.swift │ │ ├── NetworkFetchingStub.swift │ │ ├── OrderButtonViewModelTests.swift │ │ ├── OrderControllerTests.swift │ │ ├── OrderDetail.ViewModelTests.swift │ │ ├── OrderTests.swift │ │ ├── TestError.swift │ │ ├── XCTestCase+JSON.swift │ │ └── menu_item.json │ ├── AlbertosUITests │ │ ├── AlbertosUITests.swift │ │ └── Info.plist │ ├── BuildPhases │ │ └── xcsort │ ├── HippoAnalytics │ │ ├── HippoAnalytics.h │ │ ├── HippoAnalyticsClient.swift │ │ └── Info.plist │ └── HippoPayments │ │ ├── HippoPayments.h │ │ ├── HippoPaymentsConfirmationViewController.swift │ │ ├── HippoPaymentsError.swift │ │ ├── HippoPaymentsProcessor.swift │ │ ├── Info.plist │ │ └── UIViewController+Presentation.swift └── 1-end │ ├── .gitignore │ ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme │ ├── Albertos │ ├── AlbertosApp.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Color+Custom.swift │ ├── HippoPaymentsProcessor+PaymentProcessing.swift │ ├── Info.plist │ ├── MenuFetcher.swift │ ├── MenuFetching.swift │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuItemDetail.ViewModel.swift │ ├── MenuItemDetail.swift │ ├── MenuList.ViewModel.swift │ ├── MenuList.swift │ ├── MenuRow.ViewModel.swift │ ├── MenuRow.swift │ ├── MenuSection.swift │ ├── NetworkFetching.swift │ ├── Order+HippoPayments.swift │ ├── Order.swift │ ├── OrderButton.ViewModel.swift │ ├── OrderButton.swift │ ├── OrderController.swift │ ├── OrderDetail.ViewModel.swift │ ├── OrderDetail.swift │ ├── PaymentProcessing.swift │ ├── PaymentProcessingProxy.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── URLSession+NetworkFetching.swift │ ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuFetcherTests.swift │ ├── MenuFetchingStub.swift │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuItem+JSONFixture.swift │ ├── MenuItemAlternateJSONTests.swift │ ├── MenuItemDetail.ViewModelTests.swift │ ├── MenuItemTests.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuRow.ViewModelTests.swift │ ├── MenuSection+Fixture.swift │ ├── NetworkFetchingStub.swift │ ├── OrderButtonViewModelTests.swift │ ├── OrderControllerTests.swift │ ├── OrderDetail.ViewModelTests.swift │ ├── OrderTests.swift │ ├── PaymentProcessingSpy.swift │ ├── TestError.swift │ ├── XCTestCase+JSON.swift │ └── menu_item.json │ ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist │ ├── BuildPhases │ └── xcsort │ ├── HippoAnalytics │ ├── HippoAnalytics.h │ ├── HippoAnalyticsClient.swift │ └── Info.plist │ └── HippoPayments │ ├── HippoPayments.h │ ├── HippoPaymentsConfirmationViewController.swift │ ├── HippoPaymentsError.swift │ ├── HippoPaymentsProcessor.swift │ ├── Info.plist │ └── UIViewController+Presentation.swift ├── 13-testing-view-presentation ├── 0-start │ ├── .gitignore │ ├── Albertos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Albertos.xcscheme │ ├── Albertos │ │ ├── AlbertosApp.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Color+Custom.swift │ │ ├── HippoPaymentsProcessor+PaymentProcessing.swift │ │ ├── Info.plist │ │ ├── MenuFetcher.swift │ │ ├── MenuFetching.swift │ │ ├── MenuGrouping.swift │ │ ├── MenuItem.swift │ │ ├── MenuItemDetail.ViewModel.swift │ │ ├── MenuItemDetail.swift │ │ ├── MenuList.ViewModel.swift │ │ ├── MenuList.swift │ │ ├── MenuRow.ViewModel.swift │ │ ├── MenuRow.swift │ │ ├── MenuSection.swift │ │ ├── NetworkFetching.swift │ │ ├── Order+HippoPayments.swift │ │ ├── Order.swift │ │ ├── OrderButton.ViewModel.swift │ │ ├── OrderButton.swift │ │ ├── OrderController.swift │ │ ├── OrderDetail.ViewModel.swift │ │ ├── OrderDetail.swift │ │ ├── PaymentProcessing.swift │ │ ├── PaymentProcessingProxy.swift │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ └── URLSession+NetworkFetching.swift │ ├── AlbertosTests │ │ ├── Collection+Safe.swift │ │ ├── Info.plist │ │ ├── MenuFetcherTests.swift │ │ ├── MenuFetchingStub.swift │ │ ├── MenuGroupingTests.swift │ │ ├── MenuItem+Fixture.swift │ │ ├── MenuItem+JSONFixture.swift │ │ ├── MenuItemAlternateJSONTests.swift │ │ ├── MenuItemDetail.ViewModelTests.swift │ │ ├── MenuItemTests.swift │ │ ├── MenuList.ViewModelTests.swift │ │ ├── MenuRow.ViewModelTests.swift │ │ ├── MenuSection+Fixture.swift │ │ ├── NetworkFetchingStub.swift │ │ ├── OrderButtonViewModelTests.swift │ │ ├── OrderControllerTests.swift │ │ ├── OrderDetail.ViewModelTests.swift │ │ ├── OrderTests.swift │ │ ├── PaymentProcessingSpy.swift │ │ ├── TestError.swift │ │ ├── XCTestCase+JSON.swift │ │ └── menu_item.json │ ├── AlbertosUITests │ │ ├── AlbertosUITests.swift │ │ └── Info.plist │ ├── BuildPhases │ │ └── xcsort │ ├── HippoAnalytics │ │ ├── HippoAnalytics.h │ │ ├── HippoAnalyticsClient.swift │ │ └── Info.plist │ └── HippoPayments │ │ ├── HippoPayments.h │ │ ├── HippoPaymentsConfirmationViewController.swift │ │ ├── HippoPaymentsError.swift │ │ ├── HippoPaymentsProcessor.swift │ │ ├── Info.plist │ │ └── UIViewController+Presentation.swift └── 1-end │ ├── .gitignore │ ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme │ ├── Albertos │ ├── AlbertosApp.swift │ ├── Alert.ViewModel.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Color+Custom.swift │ ├── HippoPaymentsProcessor+PaymentProcessing.swift │ ├── Info.plist │ ├── MenuFetcher.swift │ ├── MenuFetching.swift │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuItemDetail.ViewModel.swift │ ├── MenuItemDetail.swift │ ├── MenuList.ViewModel.swift │ ├── MenuList.swift │ ├── MenuRow.ViewModel.swift │ ├── MenuRow.swift │ ├── MenuSection.swift │ ├── NetworkFetching.swift │ ├── Order+HippoPayments.swift │ ├── Order.swift │ ├── OrderButton.ViewModel.swift │ ├── OrderButton.swift │ ├── OrderController.swift │ ├── OrderDetail.ViewModel.swift │ ├── OrderDetail.swift │ ├── PaymentProcessing.swift │ ├── PaymentProcessingProxy.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── URLSession+NetworkFetching.swift │ ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuFetcherTests.swift │ ├── MenuFetchingStub.swift │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuItem+JSONFixture.swift │ ├── MenuItemAlternateJSONTests.swift │ ├── MenuItemDetail.ViewModelTests.swift │ ├── MenuItemTests.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuRow.ViewModelTests.swift │ ├── MenuSection+Fixture.swift │ ├── NetworkFetchingStub.swift │ ├── OrderButtonViewModelTests.swift │ ├── OrderControllerTests.swift │ ├── OrderDetail.ViewModelTests.swift │ ├── OrderTests.swift │ ├── PaymentProcessingSpy.swift │ ├── PaymentProcessingStub.swift │ ├── TestError.swift │ ├── XCTestCase+JSON.swift │ ├── XCTestCase+Timeouts.swift │ └── menu_item.json │ ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist │ ├── BuildPhases │ └── xcsort │ ├── HippoAnalytics │ ├── HippoAnalytics.h │ ├── HippoAnalyticsClient.swift │ └── Info.plist │ └── HippoPayments │ ├── HippoPayments.h │ ├── HippoPaymentsConfirmationViewController.swift │ ├── HippoPaymentsError.swift │ ├── HippoPaymentsProcessor.swift │ ├── Info.plist │ └── UIViewController+Presentation.swift ├── 14-fixing-bugs-and-changing-code ├── 0-start │ ├── .gitignore │ ├── Albertos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Albertos.xcscheme │ ├── Albertos │ │ ├── AlbertosApp.swift │ │ ├── Alert.ViewModel.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Color+Custom.swift │ │ ├── HippoPaymentsProcessor+PaymentProcessing.swift │ │ ├── Info.plist │ │ ├── MenuFetcher.swift │ │ ├── MenuFetching.swift │ │ ├── MenuGrouping.swift │ │ ├── MenuItem.swift │ │ ├── MenuItemDetail.ViewModel.swift │ │ ├── MenuItemDetail.swift │ │ ├── MenuList.ViewModel.swift │ │ ├── MenuList.swift │ │ ├── MenuRow.ViewModel.swift │ │ ├── MenuRow.swift │ │ ├── MenuSection.swift │ │ ├── NetworkFetching.swift │ │ ├── Order+HippoPayments.swift │ │ ├── Order.swift │ │ ├── OrderButton.ViewModel.swift │ │ ├── OrderButton.swift │ │ ├── OrderController.swift │ │ ├── OrderDetail.ViewModel.swift │ │ ├── OrderDetail.swift │ │ ├── PaymentProcessing.swift │ │ ├── PaymentProcessingProxy.swift │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ └── URLSession+NetworkFetching.swift │ ├── AlbertosTests │ │ ├── Collection+Safe.swift │ │ ├── Info.plist │ │ ├── MenuFetcherTests.swift │ │ ├── MenuFetchingStub.swift │ │ ├── MenuGroupingTests.swift │ │ ├── MenuItem+Fixture.swift │ │ ├── MenuItem+JSONFixture.swift │ │ ├── MenuItemAlternateJSONTests.swift │ │ ├── MenuItemDetail.ViewModelTests.swift │ │ ├── MenuItemTests.swift │ │ ├── MenuList.ViewModelTests.swift │ │ ├── MenuRow.ViewModelTests.swift │ │ ├── MenuSection+Fixture.swift │ │ ├── NetworkFetchingStub.swift │ │ ├── OrderButtonViewModelTests.swift │ │ ├── OrderControllerTests.swift │ │ ├── OrderDetail.ViewModelTests.swift │ │ ├── OrderTests.swift │ │ ├── PaymentProcessingSpy.swift │ │ ├── PaymentProcessingStub.swift │ │ ├── TestError.swift │ │ ├── XCTestCase+JSON.swift │ │ ├── XCTestCase+Timeouts.swift │ │ └── menu_item.json │ ├── AlbertosUITests │ │ ├── AlbertosUITests.swift │ │ └── Info.plist │ ├── BuildPhases │ │ └── xcsort │ ├── HippoAnalytics │ │ ├── HippoAnalytics.h │ │ ├── HippoAnalyticsClient.swift │ │ └── Info.plist │ └── HippoPayments │ │ ├── HippoPayments.h │ │ ├── HippoPaymentsConfirmationViewController.swift │ │ ├── HippoPaymentsError.swift │ │ ├── HippoPaymentsProcessor.swift │ │ ├── Info.plist │ │ └── UIViewController+Presentation.swift └── 1-end │ ├── .gitignore │ ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme │ ├── Albertos │ ├── AlbertosApp.swift │ ├── Alert.ViewModel.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Color+Custom.swift │ ├── HippoPaymentsProcessor+PaymentProcessing.swift │ ├── Info.plist │ ├── MenuFetcher.swift │ ├── MenuFetching.swift │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuItemDetail.ViewModel.swift │ ├── MenuItemDetail.swift │ ├── MenuList.ViewModel.swift │ ├── MenuList.swift │ ├── MenuRow.ViewModel.swift │ ├── MenuRow.swift │ ├── MenuSection.swift │ ├── NetworkFetching.swift │ ├── Order+HippoPayments.swift │ ├── Order.swift │ ├── OrderButton.ViewModel.swift │ ├── OrderButton.swift │ ├── OrderController.swift │ ├── OrderDetail.ViewModel.swift │ ├── OrderDetail.swift │ ├── PaymentProcessing.swift │ ├── PaymentProcessingProxy.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── URLSession+NetworkFetching.swift │ ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuFetcherTests.swift │ ├── MenuFetchingStub.swift │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuItem+JSONFixture.swift │ ├── MenuItemAlternateJSONTests.swift │ ├── MenuItemDetail.ViewModelTests.swift │ ├── MenuItemTests.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuRow.ViewModelTests.swift │ ├── MenuSection+Fixture.swift │ ├── NetworkFetchingStub.swift │ ├── OrderButtonViewModelTests.swift │ ├── OrderControllerTests.swift │ ├── OrderDetail.ViewModelTests.swift │ ├── OrderTests.swift │ ├── PaymentProcessingSpy.swift │ ├── PaymentProcessingStub.swift │ ├── TestError.swift │ ├── XCTestCase+JSON.swift │ ├── XCTestCase+Timeouts.swift │ └── menu_item.json │ ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist │ ├── BuildPhases │ └── xcsort │ ├── HippoAnalytics │ ├── HippoAnalytics.h │ ├── HippoAnalyticsClient.swift │ └── Info.plist │ └── HippoPayments │ ├── HippoPayments.h │ ├── HippoPaymentsConfirmationViewController.swift │ ├── HippoPaymentsError.swift │ ├── HippoPaymentsProcessor.swift │ ├── Info.plist │ └── UIViewController+Presentation.swift ├── 15-fake-and-dummy ├── 0-start │ ├── .gitignore │ ├── Albertos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Albertos.xcscheme │ ├── Albertos │ │ ├── AlbertosApp.swift │ │ ├── Alert.ViewModel.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Color+Custom.swift │ │ ├── HippoPaymentsProcessor+PaymentProcessing.swift │ │ ├── Info.plist │ │ ├── MenuFetcher.swift │ │ ├── MenuFetching.swift │ │ ├── MenuGrouping.swift │ │ ├── MenuItem.swift │ │ ├── MenuItemDetail.ViewModel.swift │ │ ├── MenuItemDetail.swift │ │ ├── MenuList.ViewModel.swift │ │ ├── MenuList.swift │ │ ├── MenuRow.ViewModel.swift │ │ ├── MenuRow.swift │ │ ├── MenuSection.swift │ │ ├── NetworkFetching.swift │ │ ├── Order+HippoPayments.swift │ │ ├── Order.swift │ │ ├── OrderButton.ViewModel.swift │ │ ├── OrderButton.swift │ │ ├── OrderController.swift │ │ ├── OrderDetail.ViewModel.swift │ │ ├── OrderDetail.swift │ │ ├── PaymentProcessing.swift │ │ ├── PaymentProcessingProxy.swift │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ └── URLSession+NetworkFetching.swift │ ├── AlbertosTests │ │ ├── Collection+Safe.swift │ │ ├── Info.plist │ │ ├── MenuFetcherTests.swift │ │ ├── MenuFetchingStub.swift │ │ ├── MenuGroupingTests.swift │ │ ├── MenuItem+Fixture.swift │ │ ├── MenuItem+JSONFixture.swift │ │ ├── MenuItemAlternateJSONTests.swift │ │ ├── MenuItemDetail.ViewModelTests.swift │ │ ├── MenuItemTests.swift │ │ ├── MenuList.ViewModelTests.swift │ │ ├── MenuRow.ViewModelTests.swift │ │ ├── MenuSection+Fixture.swift │ │ ├── NetworkFetchingStub.swift │ │ ├── OrderButtonViewModelTests.swift │ │ ├── OrderControllerTests.swift │ │ ├── OrderDetail.ViewModelTests.swift │ │ ├── OrderTests.swift │ │ ├── PaymentProcessingSpy.swift │ │ ├── PaymentProcessingStub.swift │ │ ├── TestError.swift │ │ ├── XCTestCase+JSON.swift │ │ ├── XCTestCase+Timeouts.swift │ │ └── menu_item.json │ ├── AlbertosUITests │ │ ├── AlbertosUITests.swift │ │ └── Info.plist │ ├── BuildPhases │ │ └── xcsort │ ├── HippoAnalytics │ │ ├── HippoAnalytics.h │ │ ├── HippoAnalyticsClient.swift │ │ └── Info.plist │ └── HippoPayments │ │ ├── HippoPayments.h │ │ ├── HippoPaymentsConfirmationViewController.swift │ │ ├── HippoPaymentsError.swift │ │ ├── HippoPaymentsProcessor.swift │ │ ├── Info.plist │ │ └── UIViewController+Presentation.swift └── 1-end │ ├── .gitignore │ ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme │ ├── Albertos │ ├── AlbertosApp.swift │ ├── Alert.ViewModel.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Color+Custom.swift │ ├── HippoPaymentsProcessor+PaymentProcessing.swift │ ├── Info.plist │ ├── MenuFetcher.swift │ ├── MenuFetching.swift │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuItemDetail.ViewModel.swift │ ├── MenuItemDetail.swift │ ├── MenuList.ViewModel.swift │ ├── MenuList.swift │ ├── MenuRow.ViewModel.swift │ ├── MenuRow.swift │ ├── MenuSection.swift │ ├── NetworkFetching.swift │ ├── Order+HippoPayments.swift │ ├── Order.swift │ ├── OrderButton.ViewModel.swift │ ├── OrderButton.swift │ ├── OrderController.swift │ ├── OrderDetail.ViewModel.swift │ ├── OrderDetail.swift │ ├── OrderStoring.swift │ ├── PaymentProcessing.swift │ ├── PaymentProcessingProxy.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── URLSession+NetworkFetching.swift │ └── UserDefaults+OrderStoring.swift │ ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuFetcherTests.swift │ ├── MenuFetchingStub.swift │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuItem+JSONFixture.swift │ ├── MenuItemAlternateJSONTests.swift │ ├── MenuItemDetail.ViewModelTests.swift │ ├── MenuItemTests.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuRow.ViewModelTests.swift │ ├── MenuSection+Fixture.swift │ ├── NetworkFetchingStub.swift │ ├── OrderButtonViewModelTests.swift │ ├── OrderControllerTests.swift │ ├── OrderDetail.ViewModelTests.swift │ ├── OrderStoringFake.swift │ ├── OrderTests.swift │ ├── PaymentProcessingDummy.swift │ ├── PaymentProcessingSpy.swift │ ├── PaymentProcessingStub.swift │ ├── TestError.swift │ ├── XCTestCase+JSON.swift │ ├── XCTestCase+Timeouts.swift │ └── menu_item.json │ ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist │ ├── BuildPhases │ └── xcsort │ ├── HippoAnalytics │ ├── HippoAnalytics.h │ ├── HippoAnalyticsClient.swift │ └── Info.plist │ └── HippoPayments │ ├── HippoPayments.h │ ├── HippoPaymentsConfirmationViewController.swift │ ├── HippoPaymentsError.swift │ ├── HippoPaymentsProcessor.swift │ ├── Info.plist │ └── UIViewController+Presentation.swift ├── 17-appendix-b-nimble-only ├── .gitignore ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme ├── Albertos │ ├── AlbertosApp.swift │ ├── Alert.ViewModel.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Color+Custom.swift │ ├── HippoPaymentsProcessor+PaymentProcessing.swift │ ├── Info.plist │ ├── MenuFetcher.swift │ ├── MenuFetching.swift │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuItemDetail.ViewModel.swift │ ├── MenuItemDetail.swift │ ├── MenuList.ViewModel.swift │ ├── MenuList.swift │ ├── MenuRow.ViewModel.swift │ ├── MenuRow.swift │ ├── MenuSection.swift │ ├── NetworkFetching.swift │ ├── Order+HippoPayments.swift │ ├── Order.swift │ ├── OrderButton.ViewModel.swift │ ├── OrderButton.swift │ ├── OrderController.swift │ ├── OrderDetail.ViewModel.swift │ ├── OrderDetail.swift │ ├── OrderStoring.swift │ ├── PaymentProcessing.swift │ ├── PaymentProcessingProxy.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── URLSession+NetworkFetching.swift │ └── UserDefaults+OrderStoring.swift ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuFetcherTests.swift │ ├── MenuFetchingStub.swift │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuItem+JSONFixture.swift │ ├── MenuItemAlternateJSONTests.swift │ ├── MenuItemDetail.ViewModelTests.swift │ ├── MenuItemTests.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuRow.ViewModelTests.swift │ ├── MenuSection+Fixture.swift │ ├── NetworkFetchingStub.swift │ ├── OrderButtonViewModelTests.swift │ ├── OrderControllerTests.swift │ ├── OrderDetail.ViewModelTests.swift │ ├── OrderStoringFake.swift │ ├── OrderTests.swift │ ├── PaymentProcessingDummy.swift │ ├── PaymentProcessingSpy.swift │ ├── PaymentProcessingStub.swift │ ├── TestError.swift │ ├── XCTestCase+JSON.swift │ ├── XCTestCase+Timeouts.swift │ └── menu_item.json ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist ├── BuildPhases │ └── xcsort ├── HippoAnalytics │ ├── HippoAnalytics.h │ ├── HippoAnalyticsClient.swift │ └── Info.plist └── HippoPayments │ ├── HippoPayments.h │ ├── HippoPaymentsConfirmationViewController.swift │ ├── HippoPaymentsError.swift │ ├── HippoPaymentsProcessor.swift │ ├── Info.plist │ └── UIViewController+Presentation.swift ├── 18-appendix-b-quick-and-nimble ├── .gitignore ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme ├── Albertos │ ├── AlbertosApp.swift │ ├── Alert.ViewModel.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Color+Custom.swift │ ├── HippoPaymentsProcessor+PaymentProcessing.swift │ ├── Info.plist │ ├── MenuFetcher.swift │ ├── MenuFetching.swift │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuItemDetail.ViewModel.swift │ ├── MenuItemDetail.swift │ ├── MenuList.ViewModel.swift │ ├── MenuList.swift │ ├── MenuRow.ViewModel.swift │ ├── MenuRow.swift │ ├── MenuSection.swift │ ├── NetworkFetching.swift │ ├── Order+HippoPayments.swift │ ├── Order.swift │ ├── OrderButton.ViewModel.swift │ ├── OrderButton.swift │ ├── OrderController.swift │ ├── OrderDetail.ViewModel.swift │ ├── OrderDetail.swift │ ├── OrderStoring.swift │ ├── PaymentProcessing.swift │ ├── PaymentProcessingProxy.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── URLSession+NetworkFetching.swift │ └── UserDefaults+OrderStoring.swift ├── AlbertosTests │ ├── Collection+Safe.swift │ ├── Info.plist │ ├── MenuFetcherTests.swift │ ├── MenuFetchingStub.swift │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuItem+JSONFixture.swift │ ├── MenuItemAlternateJSONTests.swift │ ├── MenuItemDetail.ViewModelSpec.swift │ ├── MenuItemDetail.ViewModelTests.swift │ ├── MenuItemTests.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuRow.ViewModelTests.swift │ ├── MenuSection+Fixture.swift │ ├── NetworkFetchingStub.swift │ ├── OrderButtonViewModelTests.swift │ ├── OrderControllerTests.swift │ ├── OrderDetail.ViewModelTests.swift │ ├── OrderStoringFake.swift │ ├── OrderTests.swift │ ├── PaymentProcessingDummy.swift │ ├── PaymentProcessingSpy.swift │ ├── PaymentProcessingStub.swift │ ├── TestError.swift │ ├── XCTestCase+JSON.swift │ ├── XCTestCase+Timeouts.swift │ └── menu_item.json ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist ├── BuildPhases │ └── xcsort ├── HippoAnalytics │ ├── HippoAnalytics.h │ ├── HippoAnalyticsClient.swift │ └── Info.plist └── HippoPayments │ ├── HippoPayments.h │ ├── HippoPaymentsConfirmationViewController.swift │ ├── HippoPaymentsError.swift │ ├── HippoPaymentsProcessor.swift │ ├── Info.plist │ └── UIViewController+Presentation.swift ├── 19-appendix-c-uikit ├── .gitignore ├── Albertos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── Albertos.xcscheme ├── Albertos │ ├── AlertViewModel.swift │ ├── AppCoordinator.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Collection+Safe.swift │ ├── HippoPaymentsProcessor+PaymentProcessing.swift │ ├── Info.plist │ ├── MenuFetcher.swift │ ├── MenuFetching.swift │ ├── MenuGrouping.swift │ ├── MenuItem.swift │ ├── MenuItemDetailView.swift │ ├── MenuItemDetailViewController.swift │ ├── MenuItemDetailViewModel.swift │ ├── MenuListTableViewDataSource.swift │ ├── MenuListTableViewDelegate.swift │ ├── MenuListViewController.swift │ ├── MenuListViewModel.swift │ ├── MenuRowViewModel.swift │ ├── MenuSection.swift │ ├── NetworkFetching.swift │ ├── Order+HippoPayments.swift │ ├── Order.swift │ ├── OrderButton.ViewModel.swift │ ├── OrderController.swift │ ├── OrderDetailViewController.swift │ ├── OrderDetailViewModel.swift │ ├── OrderStoring.swift │ ├── PaymentProcessing.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── UIButton+BigButtonStyle.swift │ ├── UIColor+Custom.swift │ ├── UIFont+Utils.swift │ ├── UITableViewFooterLabel.swift │ ├── UIView+AutoLayout.swift │ ├── UIViewControllerPresenting.swift │ ├── URLSession+NetworkFetching.swift │ └── UserDefaults+OrderStoring.swift ├── AlbertosTests │ ├── AppCoordinatorTests.swift │ ├── Info.plist │ ├── MenuFetcherTests.swift │ ├── MenuFetchingStub.swift │ ├── MenuGroupingTests.swift │ ├── MenuItem+Fixture.swift │ ├── MenuItem+JSONFixture.swift │ ├── MenuItemAlternateJSONTests.swift │ ├── MenuItemDetail.ViewModelTests.swift │ ├── MenuItemDetailViewControllerTests.swift │ ├── MenuItemDetailViewTests.swift │ ├── MenuItemTests.swift │ ├── MenuList.ViewModelTests.swift │ ├── MenuListTableViewDataSourceTests.swift │ ├── MenuListViewControllerTests.swift │ ├── MenuRow.ViewModelTests.swift │ ├── MenuSection+Fixture.swift │ ├── MeunListTableViewDelegateTests.swift │ ├── NetworkFetchingStub.swift │ ├── OrderButtonViewModelTests.swift │ ├── OrderControllerTests.swift │ ├── OrderDetail.ViewModelTests.swift │ ├── OrderStoringFake.swift │ ├── OrderTests.swift │ ├── PaymentProcessingDummy.swift │ ├── PaymentProcessingSpy.swift │ ├── PaymentProcessingStub.swift │ ├── SceneDelegateTests.swift │ ├── TestError.swift │ ├── XCTestCase+JSON.swift │ ├── XCTestCase+Timeouts.swift │ └── menu_item.json ├── AlbertosUITests │ ├── AlbertosUITests.swift │ └── Info.plist ├── BuildPhases │ └── xcsort ├── HippoAnalytics │ ├── HippoAnalytics.h │ ├── HippoAnalyticsClient.swift │ └── Info.plist └── HippoPayments │ ├── HippoPayments.h │ ├── HippoPaymentsConfirmationViewController.swift │ ├── HippoPaymentsError.swift │ ├── HippoPaymentsProcessor.swift │ ├── Info.plist │ └── UIViewController+Presentation.swift ├── 9781484270011.jpg ├── Contributing.md ├── LICENSE.txt └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /01-introduction/hello_world.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xcrun swift 2 | 3 | func main() { 4 | guard CommandLine.argc > 1 else { 5 | print("Hello, World!") 6 | return 7 | } 8 | 9 | print("Hello, \(CommandLine.arguments[1])!") 10 | } 11 | 12 | main() 13 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/0-start/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/0-start/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/0-start/Albertos/AlbertosApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct AlbertosApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/0-start/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/0-start/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/0-start/Albertos/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | var body: some View { 5 | Text("Hello, world!") 6 | .padding() 7 | } 8 | } 9 | 10 | struct ContentView_Previews: PreviewProvider { 11 | static var previews: some View { 12 | ContentView() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/0-start/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/1-end/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/1-end/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/1-end/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/1-end/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/1-end/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/1-end/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | } 6 | 7 | extension MenuItem: Identifiable { 8 | 9 | var id: String { name } 10 | } 11 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/1-end/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/1-end/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /04-tdd-in-the-real-world/1-end/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /05-fixtures/0-start/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /05-fixtures/0-start/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /05-fixtures/0-start/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /05-fixtures/0-start/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /05-fixtures/0-start/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /05-fixtures/0-start/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | } 6 | 7 | extension MenuItem: Identifiable { 8 | 9 | var id: String { name } 10 | } 11 | -------------------------------------------------------------------------------- /05-fixtures/0-start/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | -------------------------------------------------------------------------------- /05-fixtures/0-start/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /05-fixtures/0-start/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /05-fixtures/1-end/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /05-fixtures/1-end/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /05-fixtures/1-end/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /05-fixtures/1-end/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /05-fixtures/1-end/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /05-fixtures/1-end/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | } 7 | 8 | extension MenuItem: Identifiable { 9 | 10 | var id: String { name } 11 | } 12 | -------------------------------------------------------------------------------- /05-fixtures/1-end/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | -------------------------------------------------------------------------------- /05-fixtures/1-end/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /05-fixtures/1-end/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /05-fixtures/1-end/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false 9 | ) -> MenuItem { 10 | MenuItem(category: category, name: name, spicy: spicy) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /05-fixtures/1-end/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/0-start/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/0-start/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/0-start/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/0-start/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/0-start/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/0-start/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/0-start/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/0-start/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/0-start/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/0-start/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/0-start/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/Albertos/MenuList.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuList { 2 | 3 | struct ViewModel { 4 | 5 | let sections: [MenuSection] 6 | 7 | init( 8 | menu: [MenuItem], 9 | menuGrouping: @escaping ([MenuItem]) -> [MenuSection] = groupMenuByCategory 10 | ) { 11 | self.sections = menuGrouping(menu) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /06-testing-static-swiftui-views/1-end/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/Albertos/MenuList.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuList { 2 | 3 | struct ViewModel { 4 | 5 | let sections: [MenuSection] 6 | 7 | init( 8 | menu: [MenuItem], 9 | menuGrouping: @escaping ([MenuItem]) -> [MenuSection] = groupMenuByCategory 10 | ) { 11 | self.sections = menuGrouping(menu) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/0-start/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/Albertos/MenuFetchingPlaceholder.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | class MenuFetchingPlaceholder: MenuFetching { 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> { 6 | return Future { $0(.success(menu)) } 7 | // Use a delay to simulate async fetch 8 | .delay(for: 0.5, scheduler: RunLoop.main) 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /07-testing-dynamic-swiftui-views/1-end/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /08-stub/0-start/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /08-stub/0-start/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /08-stub/0-start/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /08-stub/0-start/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /08-stub/0-start/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /08-stub/0-start/Albertos/MenuFetchingPlaceholder.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | class MenuFetchingPlaceholder: MenuFetching { 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> { 6 | return Future { $0(.success(menu)) } 7 | // Use a delay to simulate async fetch 8 | .delay(for: 0.5, scheduler: RunLoop.main) 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /08-stub/0-start/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /08-stub/0-start/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | -------------------------------------------------------------------------------- /08-stub/0-start/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /08-stub/0-start/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /08-stub/0-start/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /08-stub/0-start/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /08-stub/0-start/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /08-stub/0-start/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /08-stub/0-start/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /08-stub/1-end/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /08-stub/1-end/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /08-stub/1-end/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /08-stub/1-end/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /08-stub/1-end/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /08-stub/1-end/Albertos/MenuFetchingPlaceholder.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | class MenuFetchingPlaceholder: MenuFetching { 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> { 6 | return Future { $0(.success(menu)) } 7 | // Use a delay to simulate async fetch 8 | .delay(for: 0.5, scheduler: RunLoop.main) 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /08-stub/1-end/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /08-stub/1-end/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | -------------------------------------------------------------------------------- /08-stub/1-end/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /08-stub/1-end/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /08-stub/1-end/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /08-stub/1-end/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /08-stub/1-end/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /08-stub/1-end/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /08-stub/1-end/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /08-stub/1-end/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/Albertos/MenuFetchingPlaceholder.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | class MenuFetchingPlaceholder: MenuFetching { 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> { 6 | return Future { $0(.success(menu)) } 7 | // Use a delay to simulate async fetch 8 | .delay(for: 0.5, scheduler: RunLoop.main) 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /09-json-decoding/0-start/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/Albertos/MenuFetchingPlaceholder.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | class MenuFetchingPlaceholder: MenuFetching { 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> { 6 | return Future { $0(.success(menu)) } 7 | // Use a delay to simulate async fetch 8 | .delay(for: 0.5, scheduler: RunLoop.main) 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Decodable {} 17 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /09-json-decoding/1-end/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /10-networking/0-start/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /10-networking/0-start/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /10-networking/0-start/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /10-networking/0-start/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /10-networking/0-start/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /10-networking/0-start/Albertos/MenuFetchingPlaceholder.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | class MenuFetchingPlaceholder: MenuFetching { 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> { 6 | return Future { $0(.success(menu)) } 7 | // Use a delay to simulate async fetch 8 | .delay(for: 0.5, scheduler: RunLoop.main) 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /10-networking/0-start/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /10-networking/0-start/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Decodable {} 17 | -------------------------------------------------------------------------------- /10-networking/0-start/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /10-networking/0-start/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /10-networking/0-start/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /10-networking/0-start/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /10-networking/0-start/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /10-networking/0-start/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /10-networking/0-start/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /10-networking/0-start/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /10-networking/0-start/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /10-networking/0-start/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos/AlbertosApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct AlbertosApp: App { 5 | 6 | var body: some Scene { 7 | WindowGroup { 8 | NavigationView { 9 | MenuList(viewModel: .init(menuFetching: MenuFetcher())) 10 | .navigationTitle("Alberto's 🇮🇹") 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Decodable {} 17 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /10-networking/1-end/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /10-networking/1-end/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /10-networking/1-end/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /10-networking/1-end/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /10-networking/1-end/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /10-networking/1-end/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /10-networking/1-end/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Decodable {} 17 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/MenuItemDetail.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuItemDetail { 2 | 3 | struct ViewModel { 4 | 5 | let name: String 6 | let spicy: String? 7 | let price: String 8 | 9 | init(item: MenuItem) { 10 | name = item.name 11 | spicy = item.spicy ? "Spicy" : .none 12 | price = "$\(String(format: "%.2f", item.price))" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/OrderButton.ViewModel.swift: -------------------------------------------------------------------------------- 1 | // This is just a placeholder to make working on the screen as we progress with the chapters easier. 2 | extension OrderButton { 3 | 4 | struct ViewModel { 5 | 6 | let text = "Your Order" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/OrderDetail.ViewModel.swift: -------------------------------------------------------------------------------- 1 | // This is just a placeholder to make working on the screen as we progress with the chapters easier. 2 | extension OrderDetail { 3 | 4 | struct ViewModel { 5 | let text = "Order Detail" 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/OrderDetail.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct OrderDetail: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/AlbertosTests/OrderButtonViewModelTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import XCTest 3 | 4 | class OrderButtonViewModelTests: XCTestCase { 5 | 6 | // This is just a placeholder to make adding tests as we progress with the chapters easier. 7 | } 8 | 9 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/AlbertosTests/OrderDetail.ViewModelTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import XCTest 3 | 4 | class OrderDetailViewModelTests: XCTestCase { 5 | 6 | // This is just a placeholder to make adding tests as we progress with the chapters easier. 7 | } 8 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/AlbertosTests/OrderTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import XCTest 3 | 4 | class OrderTests: XCTestCase { 5 | 6 | func testTotalSumsPricesOfEachItem() { 7 | let order = Order( 8 | items: [.fixture(price: 1.0), .fixture(price: 2.0), .fixture(price: 3.5)] 9 | ) 10 | 11 | XCTAssertEqual(order.total, 6.5) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/0-start/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Decodable {} 17 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/OrderButton.ViewModel.swift: -------------------------------------------------------------------------------- 1 | // This is just a placeholder to make working on the screen as we progress with the chapters easier. 2 | extension OrderButton { 3 | 4 | struct ViewModel { 5 | 6 | let text = "Your Order" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/OrderDetail.ViewModel.swift: -------------------------------------------------------------------------------- 1 | // This is just a placeholder to make working on the screen as we progress with the chapters easier. 2 | extension OrderDetail { 3 | 4 | struct ViewModel { 5 | let text = "Order Detail" 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/OrderDetail.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct OrderDetail: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/AlbertosTests/OrderButtonViewModelTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import XCTest 3 | 4 | class OrderButtonViewModelTests: XCTestCase { 5 | 6 | // This is just a placeholder to make adding tests as we progress with the chapters easier. 7 | } 8 | 9 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/AlbertosTests/OrderDetail.ViewModelTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import XCTest 3 | 4 | class OrderDetailViewModelTests: XCTestCase { 5 | 6 | // This is just a placeholder to make adding tests as we progress with the chapters easier. 7 | } 8 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/AlbertosTests/OrderTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import XCTest 3 | 4 | class OrderTests: XCTestCase { 5 | 6 | func testTotalSumsPricesOfEachItem() { 7 | let order = Order( 8 | items: [.fixture(price: 1.0), .fixture(price: 2.0), .fixture(price: 3.5)] 9 | ) 10 | 11 | XCTAssertEqual(order.total, 6.5) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /11-dependency-injection-with-environment-object/1-end/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Decodable {} 17 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /12-spy/0-start/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /12-spy/0-start/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /12-spy/0-start/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /12-spy/0-start/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /12-spy/0-start/AlbertosTests/OrderTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import XCTest 3 | 4 | class OrderTests: XCTestCase { 5 | 6 | func testTotalSumsPricesOfEachItem() { 7 | let order = Order( 8 | items: [.fixture(price: 1.0), .fixture(price: 2.0), .fixture(price: 3.5)] 9 | ) 10 | 11 | XCTAssertEqual(order.total, 6.5) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /12-spy/0-start/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /12-spy/0-start/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /12-spy/0-start/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /12-spy/0-start/HippoPayments/HippoPayments.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for HippoPayments. 4 | FOUNDATION_EXPORT double HippoPaymentsVersionNumber; 5 | 6 | //! Project version string for HippoPayments. 7 | FOUNDATION_EXPORT const unsigned char HippoPaymentsVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | -------------------------------------------------------------------------------- /12-spy/0-start/HippoPayments/HippoPaymentsError.swift: -------------------------------------------------------------------------------- 1 | public enum HippoPaymentsError: Error { 2 | case genericError 3 | } 4 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Decodable {} 17 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/Order+HippoPayments.swift: -------------------------------------------------------------------------------- 1 | extension Order { 2 | 3 | var hippoPaymentsPayload: [String: Any] { ["items": items.map { $0.name }] } 4 | } 5 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | 8 | extension Order: Equatable {} 9 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/PaymentProcessing.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol PaymentProcessing { 4 | 5 | func process(order: Order) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /12-spy/1-end/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /12-spy/1-end/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /12-spy/1-end/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /12-spy/1-end/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /12-spy/1-end/AlbertosTests/PaymentProcessingSpy.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import Combine 3 | 4 | class PaymentProcessingSpy: PaymentProcessing { 5 | 6 | private(set) var receivedOrder: Order? 7 | 8 | func process(order: Order) -> AnyPublisher { 9 | receivedOrder = order 10 | 11 | return Result.success(()).publisher.eraseToAnyPublisher() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /12-spy/1-end/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /12-spy/1-end/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /12-spy/1-end/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /12-spy/1-end/HippoPayments/HippoPayments.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for HippoPayments. 4 | FOUNDATION_EXPORT double HippoPaymentsVersionNumber; 5 | 6 | //! Project version string for HippoPayments. 7 | FOUNDATION_EXPORT const unsigned char HippoPaymentsVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | -------------------------------------------------------------------------------- /12-spy/1-end/HippoPayments/HippoPaymentsError.swift: -------------------------------------------------------------------------------- 1 | public enum HippoPaymentsError: Error { 2 | case genericError 3 | } 4 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Decodable {} 17 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/Order+HippoPayments.swift: -------------------------------------------------------------------------------- 1 | extension Order { 2 | 3 | var hippoPaymentsPayload: [String: Any] { ["items": items.map { $0.name }] } 4 | } 5 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | 8 | extension Order: Equatable {} 9 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/PaymentProcessing.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol PaymentProcessing { 4 | 5 | func process(order: Order) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/AlbertosTests/PaymentProcessingSpy.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import Combine 3 | 4 | class PaymentProcessingSpy: PaymentProcessing { 5 | 6 | private(set) var receivedOrder: Order? 7 | 8 | func process(order: Order) -> AnyPublisher { 9 | receivedOrder = order 10 | 11 | return Result.success(()).publisher.eraseToAnyPublisher() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /13-testing-view-presentation/0-start/HippoPayments/HippoPaymentsError.swift: -------------------------------------------------------------------------------- 1 | public enum HippoPaymentsError: Error { 2 | case genericError 3 | } 4 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/Alert.ViewModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Alert { 4 | 5 | struct ViewModel: Identifiable { 6 | 7 | let title: String 8 | let message: String 9 | let buttonText: String 10 | let buttonAction: (() -> Void)? 11 | 12 | var id: String { title + message + buttonText } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Decodable {} 17 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/Order+HippoPayments.swift: -------------------------------------------------------------------------------- 1 | extension Order { 2 | 3 | var hippoPaymentsPayload: [String: Any] { ["items": items.map { $0.name }] } 4 | } 5 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | 8 | extension Order: Equatable {} 9 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/PaymentProcessing.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol PaymentProcessing { 4 | 5 | func process(order: Order) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/AlbertosTests/PaymentProcessingSpy.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import Combine 3 | 4 | class PaymentProcessingSpy: PaymentProcessing { 5 | 6 | private(set) var receivedOrder: Order? 7 | 8 | func process(order: Order) -> AnyPublisher { 9 | receivedOrder = order 10 | 11 | return Result.success(()).publisher.eraseToAnyPublisher() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/AlbertosTests/XCTestCase+Timeouts.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | /// Using a wait time of around 1 second seems to result in occasional 6 | /// test timeout failures when using `XCTNSPredicateExpectation`s. 7 | var timeoutForPredicateExpectations: Double { 2.0 } 8 | } 9 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /13-testing-view-presentation/1-end/HippoPayments/HippoPaymentsError.swift: -------------------------------------------------------------------------------- 1 | public enum HippoPaymentsError: Error { 2 | case genericError 3 | } 4 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/Alert.ViewModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Alert { 4 | 5 | struct ViewModel: Identifiable { 6 | 7 | let title: String 8 | let message: String 9 | let buttonText: String 10 | let buttonAction: (() -> Void)? 11 | 12 | var id: String { title + message + buttonText } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Decodable {} 17 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🌶" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/Order+HippoPayments.swift: -------------------------------------------------------------------------------- 1 | extension Order { 2 | 3 | var hippoPaymentsPayload: [String: Any] { ["items": items.map { $0.name }] } 4 | } 5 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | 8 | extension Order: Equatable {} 9 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/PaymentProcessing.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol PaymentProcessing { 4 | 5 | func process(order: Order) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/AlbertosTests/PaymentProcessingSpy.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import Combine 3 | 4 | class PaymentProcessingSpy: PaymentProcessing { 5 | 6 | private(set) var receivedOrder: Order? 7 | 8 | func process(order: Order) -> AnyPublisher { 9 | receivedOrder = order 10 | 11 | return Result.success(()).publisher.eraseToAnyPublisher() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/AlbertosTests/XCTestCase+Timeouts.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | /// Using a wait time of around 1 second seems to result in occasional 6 | /// test timeout failures when using `XCTNSPredicateExpectation`s. 7 | var timeoutForPredicateExpectations: Double { 2.0 } 8 | } 9 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/0-start/HippoPayments/HippoPaymentsError.swift: -------------------------------------------------------------------------------- 1 | public enum HippoPaymentsError: Error { 2 | case genericError 3 | } 4 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/Alert.ViewModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Alert { 4 | 5 | struct ViewModel: Identifiable { 6 | 7 | let title: String 8 | let message: String 9 | let buttonText: String 10 | let buttonAction: (() -> Void)? 11 | 12 | var id: String { title + message + buttonText } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Decodable {} 17 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🔥" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/Order+HippoPayments.swift: -------------------------------------------------------------------------------- 1 | extension Order { 2 | 3 | var hippoPaymentsPayload: [String: Any] { ["items": items.map { $0.name }] } 4 | } 5 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | 8 | extension Order: Equatable {} 9 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/PaymentProcessing.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol PaymentProcessing { 4 | 5 | func process(order: Order) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/AlbertosTests/PaymentProcessingSpy.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import Combine 3 | 4 | class PaymentProcessingSpy: PaymentProcessing { 5 | 6 | private(set) var receivedOrder: Order? 7 | 8 | func process(order: Order) -> AnyPublisher { 9 | receivedOrder = order 10 | 11 | return Result.success(()).publisher.eraseToAnyPublisher() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/AlbertosTests/XCTestCase+Timeouts.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | /// Using a wait time of around 1 second seems to result in occasional 6 | /// test timeout failures when using `XCTNSPredicateExpectation`s. 7 | var timeoutForPredicateExpectations: Double { 2.0 } 8 | } 9 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /14-fixing-bugs-and-changing-code/1-end/HippoPayments/HippoPaymentsError.swift: -------------------------------------------------------------------------------- 1 | public enum HippoPaymentsError: Error { 2 | case genericError 3 | } 4 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/Alert.ViewModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Alert { 4 | 5 | struct ViewModel: Identifiable { 6 | 7 | let title: String 8 | let message: String 9 | let buttonText: String 10 | let buttonAction: (() -> Void)? 11 | 12 | var id: String { title + message + buttonText } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Decodable {} 17 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🔥" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/Order+HippoPayments.swift: -------------------------------------------------------------------------------- 1 | extension Order { 2 | 3 | var hippoPaymentsPayload: [String: Any] { ["items": items.map { $0.name }] } 4 | } 5 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | 8 | extension Order: Equatable {} 9 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/PaymentProcessing.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol PaymentProcessing { 4 | 5 | func process(order: Order) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/AlbertosTests/PaymentProcessingSpy.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import Combine 3 | 4 | class PaymentProcessingSpy: PaymentProcessing { 5 | 6 | private(set) var receivedOrder: Order? 7 | 8 | func process(order: Order) -> AnyPublisher { 9 | receivedOrder = order 10 | 11 | return Result.success(()).publisher.eraseToAnyPublisher() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/AlbertosTests/XCTestCase+Timeouts.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | /// Using a wait time of around 1 second seems to result in occasional 6 | /// test timeout failures when using `XCTNSPredicateExpectation`s. 7 | var timeoutForPredicateExpectations: Double { 2.0 } 8 | } 9 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/HippoPayments/HippoPayments.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for HippoPayments. 4 | FOUNDATION_EXPORT double HippoPaymentsVersionNumber; 5 | 6 | //! Project version string for HippoPayments. 7 | FOUNDATION_EXPORT const unsigned char HippoPaymentsVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | -------------------------------------------------------------------------------- /15-fake-and-dummy/0-start/HippoPayments/HippoPaymentsError.swift: -------------------------------------------------------------------------------- 1 | public enum HippoPaymentsError: Error { 2 | case genericError 3 | } 4 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/Alert.ViewModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Alert { 4 | 5 | struct ViewModel: Identifiable { 6 | 7 | let title: String 8 | let message: String 9 | let buttonText: String 10 | let buttonAction: (() -> Void)? 11 | 12 | var id: String { title + message + buttonText } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Codable {} 17 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🔥" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/Order+HippoPayments.swift: -------------------------------------------------------------------------------- 1 | extension Order { 2 | 3 | var hippoPaymentsPayload: [String: Any] { ["items": items.map { $0.name }] } 4 | } 5 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | 8 | extension Order: Codable, Equatable {} 9 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/OrderStoring.swift: -------------------------------------------------------------------------------- 1 | protocol OrderStoring { 2 | 3 | func getOrder() -> Order 4 | 5 | func updateOrder(_ order: Order) 6 | } 7 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/PaymentProcessing.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol PaymentProcessing { 4 | 5 | func process(order: Order) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/AlbertosTests/OrderStoringFake.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | class OrderStoringFake: OrderStoring { 4 | 5 | private var order: Order = Order(items: []) 6 | 7 | func getOrder() -> Order { 8 | return order 9 | } 10 | 11 | func updateOrder(_ order: Order) { 12 | self.order = order 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/AlbertosTests/PaymentProcessingSpy.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import Combine 3 | 4 | class PaymentProcessingSpy: PaymentProcessing { 5 | 6 | private(set) var receivedOrder: Order? 7 | 8 | func process(order: Order) -> AnyPublisher { 9 | receivedOrder = order 10 | 11 | return Result.success(()).publisher.eraseToAnyPublisher() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/AlbertosTests/XCTestCase+Timeouts.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | /// Using a wait time of around 1 second seems to result in occasional 6 | /// test timeout failures when using `XCTNSPredicateExpectation`s. 7 | var timeoutForPredicateExpectations: Double { 2.0 } 8 | } 9 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/HippoPayments/HippoPayments.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for HippoPayments. 4 | FOUNDATION_EXPORT double HippoPaymentsVersionNumber; 5 | 6 | //! Project version string for HippoPayments. 7 | FOUNDATION_EXPORT const unsigned char HippoPaymentsVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | -------------------------------------------------------------------------------- /15-fake-and-dummy/1-end/HippoPayments/HippoPaymentsError.swift: -------------------------------------------------------------------------------- 1 | public enum HippoPaymentsError: Error { 2 | case genericError 3 | } 4 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/Alert.ViewModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Alert { 4 | 5 | struct ViewModel: Identifiable { 6 | 7 | let title: String 8 | let message: String 9 | let buttonText: String 10 | let buttonAction: (() -> Void)? 11 | 12 | var id: String { title + message + buttonText } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Codable {} 17 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🔥" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/Order+HippoPayments.swift: -------------------------------------------------------------------------------- 1 | extension Order { 2 | 3 | var hippoPaymentsPayload: [String: Any] { ["items": items.map { $0.name }] } 4 | } 5 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | 8 | extension Order: Codable, Equatable {} 9 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/OrderStoring.swift: -------------------------------------------------------------------------------- 1 | protocol OrderStoring { 2 | 3 | func getOrder() -> Order 4 | 5 | func updateOrder(_ order: Order) 6 | } 7 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/PaymentProcessing.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol PaymentProcessing { 4 | 5 | func process(order: Order) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/AlbertosTests/OrderStoringFake.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | class OrderStoringFake: OrderStoring { 4 | 5 | private var order: Order = Order(items: []) 6 | 7 | func getOrder() -> Order { 8 | return order 9 | } 10 | 11 | func updateOrder(_ order: Order) { 12 | self.order = order 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/AlbertosTests/PaymentProcessingSpy.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import Combine 3 | 4 | class PaymentProcessingSpy: PaymentProcessing { 5 | 6 | private(set) var receivedOrder: Order? 7 | 8 | func process(order: Order) -> AnyPublisher { 9 | receivedOrder = order 10 | 11 | return Result.success(()).publisher.eraseToAnyPublisher() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/AlbertosTests/XCTestCase+Timeouts.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | /// Using a wait time of around 1 second seems to result in occasional 6 | /// test timeout failures when using `XCTNSPredicateExpectation`s. 7 | var timeoutForPredicateExpectations: Double { 2.0 } 8 | } 9 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/HippoPayments/HippoPayments.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for HippoPayments. 4 | FOUNDATION_EXPORT double HippoPaymentsVersionNumber; 5 | 6 | //! Project version string for HippoPayments. 7 | FOUNDATION_EXPORT const unsigned char HippoPaymentsVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | -------------------------------------------------------------------------------- /17-appendix-b-nimble-only/HippoPayments/HippoPaymentsError.swift: -------------------------------------------------------------------------------- 1 | public enum HippoPaymentsError: Error { 2 | case genericError 3 | } 4 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/Alert.ViewModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Alert { 4 | 5 | struct ViewModel: Identifiable { 6 | 7 | let title: String 8 | let message: String 9 | let buttonText: String 10 | let buttonAction: (() -> Void)? 11 | 12 | var id: String { title + message + buttonText } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Identifiable { 10 | 11 | var id: String { name } 12 | } 13 | 14 | extension MenuItem: Equatable {} 15 | 16 | extension MenuItem: Codable {} 17 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/MenuRow.ViewModel.swift: -------------------------------------------------------------------------------- 1 | extension MenuRow { 2 | 3 | struct ViewModel { 4 | 5 | let text: String 6 | 7 | init(item: MenuItem) { 8 | text = item.spicy ? "\(item.name) 🔥" : item.name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/MenuRow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MenuRow: View { 4 | 5 | let viewModel: ViewModel 6 | 7 | var body: some View { 8 | Text(viewModel.text) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Identifiable { 8 | 9 | var id: String { category } 10 | } 11 | 12 | extension MenuSection: Equatable {} 13 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/Order+HippoPayments.swift: -------------------------------------------------------------------------------- 1 | extension Order { 2 | 3 | var hippoPaymentsPayload: [String: Any] { ["items": items.map { $0.name }] } 4 | } 5 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | 8 | extension Order: Codable, Equatable {} 9 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/OrderStoring.swift: -------------------------------------------------------------------------------- 1 | protocol OrderStoring { 2 | 3 | func getOrder() -> Order 4 | 5 | func updateOrder(_ order: Order) 6 | } 7 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/PaymentProcessing.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol PaymentProcessing { 4 | 5 | func process(order: Order) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/AlbertosTests/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/AlbertosTests/OrderStoringFake.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | class OrderStoringFake: OrderStoring { 4 | 5 | private var order: Order = Order(items: []) 6 | 7 | func getOrder() -> Order { 8 | return order 9 | } 10 | 11 | func updateOrder(_ order: Order) { 12 | self.order = order 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/AlbertosTests/PaymentProcessingSpy.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import Combine 3 | 4 | class PaymentProcessingSpy: PaymentProcessing { 5 | 6 | private(set) var receivedOrder: Order? 7 | 8 | func process(order: Order) -> AnyPublisher { 9 | receivedOrder = order 10 | 11 | return Result.success(()).publisher.eraseToAnyPublisher() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/AlbertosTests/XCTestCase+Timeouts.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | /// Using a wait time of around 1 second seems to result in occasional 6 | /// test timeout failures when using `XCTNSPredicateExpectation`s. 7 | var timeoutForPredicateExpectations: Double { 2.0 } 8 | } 9 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /18-appendix-b-quick-and-nimble/HippoPayments/HippoPaymentsError.swift: -------------------------------------------------------------------------------- 1 | public enum HippoPaymentsError: Error { 2 | case genericError 3 | } 4 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/AlertViewModel.swift: -------------------------------------------------------------------------------- 1 | struct AlertViewModel { 2 | 3 | let title: String 4 | let message: String 5 | let buttonText: String 6 | let buttonAction: (() -> Void)? 7 | } 8 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | 3 | /// Returns the element at the specified index if it is within range, otherwise nil. 4 | subscript(safe index: Index) -> Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/MenuFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol MenuFetching { 4 | 5 | func fetchMenu() -> AnyPublisher<[MenuItem], Error> 6 | } 7 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/MenuGrouping.swift: -------------------------------------------------------------------------------- 1 | func groupMenuByCategory(_ menu: [MenuItem]) -> [MenuSection] { 2 | guard menu.isEmpty == false else { return [] } 3 | 4 | return Dictionary(grouping: menu, by: { $0.category }) 5 | .map { key, value in MenuSection(category: key, items: value) } 6 | .sorted { $0.category > $1.category } 7 | } 8 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/MenuItem.swift: -------------------------------------------------------------------------------- 1 | struct MenuItem { 2 | 3 | let category: String 4 | let name: String 5 | let spicy: Bool 6 | let price: Double 7 | } 8 | 9 | extension MenuItem: Equatable {} 10 | 11 | extension MenuItem: Codable {} 12 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/MenuRowViewModel.swift: -------------------------------------------------------------------------------- 1 | struct MenuRowViewModel { 2 | 3 | let text: String 4 | 5 | init(item: MenuItem) { 6 | text = item.spicy ? "\(item.name) 🔥" : item.name 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/MenuSection.swift: -------------------------------------------------------------------------------- 1 | struct MenuSection { 2 | 3 | let category: String 4 | let items: [MenuItem] 5 | } 6 | 7 | extension MenuSection: Equatable {} 8 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | protocol NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/Order+HippoPayments.swift: -------------------------------------------------------------------------------- 1 | extension Order { 2 | 3 | var hippoPaymentsPayload: [String: Any] { ["items": items.map { $0.name }] } 4 | } 5 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/Order.swift: -------------------------------------------------------------------------------- 1 | struct Order { 2 | 3 | let items: [MenuItem] 4 | 5 | var total: Double { items.reduce(0) { $0 + $1.price } } 6 | } 7 | 8 | extension Order: Codable, Equatable {} 9 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/OrderStoring.swift: -------------------------------------------------------------------------------- 1 | protocol OrderStoring { 2 | 3 | func getOrder() -> Order 4 | 5 | func updateOrder(_ order: Order) 6 | } 7 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/PaymentProcessing.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol PaymentProcessing { 4 | 5 | func process(order: Order) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/UIFont+Utils.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIFont { 4 | 5 | func adding(_ trait: UIFontDescriptor.SymbolicTraits) -> UIFont { 6 | guard let fontDescriptor = fontDescriptor.withSymbolicTraits(trait) else { 7 | return self 8 | } 9 | 10 | // size = 0 means "keep the current size" 11 | return UIFont(descriptor: fontDescriptor, size: 0) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/Albertos/URLSession+NetworkFetching.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | extension URLSession: NetworkFetching { 5 | 6 | func load(_ request: URLRequest) -> AnyPublisher { 7 | return dataTaskPublisher(for: request) 8 | .map { $0.data } 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/AlbertosTests/MenuItem+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuItem { 4 | 5 | static func fixture( 6 | category: String = "category", 7 | name: String = "name", 8 | spicy: Bool = false, 9 | price: Double = 1.0 10 | ) -> MenuItem { 11 | MenuItem(category: category, name: name, spicy: spicy, price: price) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/AlbertosTests/MenuSection+Fixture.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | extension MenuSection { 4 | 5 | static func fixture( 6 | category: String = "a category", 7 | items: [MenuItem] = [.fixture()] 8 | ) -> MenuSection { 9 | return MenuSection(category: category, items: items) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/AlbertosTests/OrderStoringFake.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | 3 | class OrderStoringFake: OrderStoring { 4 | 5 | private var order: Order = Order(items: []) 6 | 7 | func getOrder() -> Order { 8 | return order 9 | } 10 | 11 | func updateOrder(_ order: Order) { 12 | self.order = order 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/AlbertosTests/PaymentProcessingSpy.swift: -------------------------------------------------------------------------------- 1 | @testable import Albertos 2 | import Combine 3 | 4 | class PaymentProcessingSpy: PaymentProcessing { 5 | 6 | private(set) var receivedOrder: Order? 7 | 8 | func process(order: Order) -> AnyPublisher { 9 | receivedOrder = order 10 | 11 | return Result.success(()).publisher.eraseToAnyPublisher() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/AlbertosTests/TestError.swift: -------------------------------------------------------------------------------- 1 | struct TestError: Equatable, Error { 2 | let id: Int 3 | } 4 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/AlbertosTests/XCTestCase+JSON.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | func dataFromJSONFileNamed(_ name: String) throws -> Data { 6 | let url = try XCTUnwrap( 7 | Bundle(for: type(of: self)).url(forResource: name, withExtension: "json") 8 | ) 9 | return try Data(contentsOf: url) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/AlbertosTests/XCTestCase+Timeouts.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | 5 | /// Using a wait time of around 1 second seems to result in occasional 6 | /// test timeout failures when using `XCTNSPredicateExpectation`s. 7 | var timeoutForPredicateExpectations: Double { 2.0 } 8 | } 9 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/AlbertosTests/menu_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a name", 3 | "category": "a category", 4 | "spicy": true, 5 | "price": 1.0 6 | } 7 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/HippoPayments/HippoPayments.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for HippoPayments. 4 | FOUNDATION_EXPORT double HippoPaymentsVersionNumber; 5 | 6 | //! Project version string for HippoPayments. 7 | FOUNDATION_EXPORT const unsigned char HippoPaymentsVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | -------------------------------------------------------------------------------- /19-appendix-c-uikit/HippoPayments/HippoPaymentsError.swift: -------------------------------------------------------------------------------- 1 | public enum HippoPaymentsError: Error { 2 | case genericError 3 | } 4 | -------------------------------------------------------------------------------- /9781484270011.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/Test-Driven-Development-in-Swift/13c24681fb0df9f109bd8bdf9575b6b6d90682a3/9781484270011.jpg --------------------------------------------------------------------------------