├── .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
--------------------------------------------------------------------------------