├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .package.resolved ├── .periphery.yml ├── .swift-version ├── .swiftformat ├── App ├── Mocks │ └── .gitkeep ├── Resources │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── AppIcon.png │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Blueprints.xcassets │ │ ├── Contents.json │ │ ├── aw.imageset │ │ │ ├── Contents.json │ │ │ └── aw.pdf │ │ ├── h1.imageset │ │ │ ├── Contents.json │ │ │ └── h1.pdf │ │ ├── h2.imageset │ │ │ ├── Contents.json │ │ │ └── h2.pdf │ │ ├── j.imageset │ │ │ ├── Contents.json │ │ │ └── j.pdf │ │ ├── k1-1.imageset │ │ │ ├── Contents.json │ │ │ └── k1-1.pdf │ │ ├── k1-2.imageset │ │ │ ├── Contents.json │ │ │ └── k1-2.pdf │ │ ├── k2.imageset │ │ │ ├── Contents.json │ │ │ └── k2.pdf │ │ ├── k3.imageset │ │ │ ├── Contents.json │ │ │ └── k3.pdf │ │ ├── k4.imageset │ │ │ ├── Contents.json │ │ │ └── k4.pdf │ │ └── u.imageset │ │ │ ├── Contents.json │ │ │ └── u.pdf │ ├── Buildings │ │ ├── aw.json │ │ ├── f.json │ │ ├── h.json │ │ ├── j.json │ │ ├── k.json │ │ ├── s.json │ │ └── u.json │ ├── FOSDEM.entitlements │ ├── Info.plist │ ├── LaunchScreen.storyboard │ ├── More.xcassets │ │ ├── Contents.json │ │ ├── contribute.imageset │ │ │ ├── Contents.json │ │ │ ├── contribute.png │ │ │ ├── contribute@2x.png │ │ │ └── contribute@3x.png │ │ ├── devrooms.imageset │ │ │ ├── Contents.json │ │ │ ├── devrooms.png │ │ │ ├── devrooms@2x.png │ │ │ └── devrooms@3x.png │ │ ├── document.imageset │ │ │ ├── Contents.json │ │ │ ├── acknowledgements.png │ │ │ ├── acknowledgements@2x.png │ │ │ └── acknowledgements@3x.png │ │ ├── history.imageset │ │ │ ├── Contents.json │ │ │ ├── history.png │ │ │ ├── history@2x.png │ │ │ └── history@3x.png │ │ ├── timezone.imageset │ │ │ ├── Contents.json │ │ │ ├── timezone.png │ │ │ ├── timezone@2x.png │ │ │ └── timezone@3x.png │ │ ├── transportation.imageset │ │ │ ├── Contents.json │ │ │ ├── transportation.png │ │ │ ├── transportation@2x.png │ │ │ └── transportation@3x.png │ │ ├── video.imageset │ │ │ ├── Contents.json │ │ │ ├── video.png │ │ │ ├── video@2x.png │ │ │ └── video@3x.png │ │ └── years.imageset │ │ │ ├── Contents.json │ │ │ ├── years.png │ │ │ ├── years@2x.png │ │ │ └── years@3x.png │ ├── More │ │ ├── bus-tram.html │ │ ├── car.html │ │ ├── devrooms.html │ │ ├── history.html │ │ ├── legal.html │ │ ├── plane.html │ │ ├── taxi.html │ │ └── train.html │ ├── Search.xcassets │ │ ├── Contents.json │ │ └── logo.imageset │ │ │ ├── Contents.json │ │ │ ├── logo.png │ │ │ ├── logo@2x.png │ │ │ └── logo@3x.png │ ├── ULB.gpx │ ├── acknowledgements.json │ ├── db.sqlite │ └── en.lproj │ │ └── Localizable.strings ├── SnapshotTests │ ├── AcknowledgementsViewControllerTests.swift │ ├── BlueprintViewControllerTests.swift │ ├── ErrorViewControllerTests.swift │ ├── Event+Extensions.swift │ ├── EventControllerTests.swift │ ├── EventViewControllerTests.swift │ ├── EventsViewControllerTests.swift │ ├── MapContainerViewControllerTests.swift │ ├── MapViewControllerTests.swift │ ├── MoreViewControllerTests.swift │ ├── TextViewControllerTests.swift │ ├── TracksViewControllerTests.swift │ ├── TransportationController.swift │ ├── TransportationViewControllerTests.swift │ ├── UIView+Extensions.swift │ ├── UIViewController+Extensions.swift │ ├── UIViewControllerTransitionCoordinatorMock.swift │ ├── WelcomeViewControllerTests.swift │ ├── YearsViewControllerTests.swift │ └── __Snapshots__ │ │ ├── AcknowledgementsViewControllerTests │ │ └── testAppearance.1.png │ │ ├── BlueprintViewControllerTests │ │ ├── testEmbedded.1.png │ │ ├── testEmbedded.2.png │ │ ├── testEmbedded.3.png │ │ ├── testEmpty.1.png │ │ ├── testFullscreen.1.png │ │ ├── testFullscreen.2.png │ │ └── testFullscreen.3.png │ │ ├── ErrorViewControllerTests │ │ ├── testAppearance.1.png │ │ └── testAppearance.2.png │ │ ├── EventControllerTests │ │ ├── testAppearance.1.png │ │ ├── testAppearance.2.png │ │ ├── testAppearance.3.png │ │ ├── testAppearance.4.png │ │ ├── testAppearance.5.png │ │ ├── testFavoriteEvents.1.png │ │ ├── testFavoriteNotification.1.png │ │ ├── testFavoriteNotification.2.png │ │ ├── testFavoriteNotification.3.png │ │ ├── testPlaybackPositionUpdatesUI.1.png │ │ ├── testPlaybackPositionUpdatesUI.2.png │ │ └── testPlaybackPositionUpdatesUI.3.png │ │ ├── EventViewControllerTests │ │ ├── testAppearance.1.png │ │ ├── testAppearance.2.png │ │ ├── testAppearance.3.png │ │ ├── testAppearance.4.png │ │ ├── testEvents.1.png │ │ └── testEvents.2.png │ │ ├── EventsViewControllerTests │ │ ├── testAppearance.1.png │ │ ├── testAppearance.2.png │ │ ├── testLive.1.png │ │ ├── testLive.2.png │ │ ├── testLive.3.png │ │ ├── testUpdates.1.png │ │ ├── testUpdates.2.png │ │ └── testUpdates.3.png │ │ ├── MapContainerViewControllerTests │ │ ├── testAppearance.1.png │ │ ├── testAppearance.2.png │ │ ├── testAppearance.3.png │ │ ├── testAppearance.4.png │ │ ├── testAppearance.5.png │ │ ├── testAppearance.6.png │ │ ├── testAppearance.7.png │ │ ├── testAppearance.8.png │ │ ├── testAppearance.9.png │ │ ├── testScrolling.1.png │ │ └── testScrolling.2.png │ │ ├── MapViewControllerTests │ │ ├── testAppearance.1.png │ │ ├── testAppearance.2.png │ │ ├── testAppearance.3.png │ │ ├── testAppearance.4.png │ │ └── testAppearance.5.png │ │ ├── MoreViewControllerTests │ │ └── testAppearance.1.png │ │ ├── TextViewControllerTests │ │ └── testAppearance.1.png │ │ ├── TracksViewControllerTests │ │ ├── testAppearance.1.png │ │ ├── testReloadData.1.png │ │ ├── testReloadData.2.png │ │ ├── testScroll.1.png │ │ ├── testUpdates.1.png │ │ ├── testUpdates.2.png │ │ ├── testUpdates.3.png │ │ └── testUpdates.4.png │ │ ├── TransportationController │ │ ├── testAppearance.1.png │ │ └── testEvents.1.png │ │ ├── TransportationViewControllerTests │ │ └── testAppearance.1.png │ │ ├── WelcomeViewControllerTests │ │ ├── testAppearance.1.png │ │ ├── testAppearance.2.png │ │ └── testAppearance.3.png │ │ └── YearsViewControllerTests │ │ ├── testAppearance.1.png │ │ ├── testAppearance.2.png │ │ └── testAppearance.3.png ├── Sources │ ├── App.swift │ ├── AppDelegate.swift │ ├── Controllers │ │ ├── AgendaController.swift │ │ ├── ApplicationController.swift │ │ ├── EventController.swift │ │ ├── InfoController.swift │ │ ├── MapController.swift │ │ ├── MoreController.swift │ │ ├── SearchController.swift │ │ ├── TrackController.swift │ │ ├── TransportationController.swift │ │ ├── VideosController.swift │ │ ├── YearController.swift │ │ └── YearsController.swift │ ├── Debug │ │ ├── BuildingsEditorViewController.swift │ │ ├── EventsSlideshowController.swift │ │ └── HTMLRenderingController.swift │ ├── Extensions │ │ ├── AVAudioSession+Extensions.swift │ │ ├── Bundle+Extensions.swift │ │ ├── Calendar+Extensions.swift │ │ ├── MKAnnotationView+Extensions.swift │ │ ├── TimeZone+Extensions.swift │ │ ├── UIAlertController+Extensions.swift │ │ ├── UIFont+Extensions.swift │ │ ├── UIImage+Extensions.swift │ │ ├── UITableViewCell+Extensions.swift │ │ ├── UITableViewController+Extensions.swift │ │ ├── UITableViewHeaderFooterView+Extensions.swift │ │ ├── UITraitCollection+Extensions.swift │ │ └── URL+Extensions.swift │ ├── Models │ │ ├── Acknowledgement.swift │ │ ├── AppStoreSearchResponse.swift │ │ ├── AppStoreSearchResult.swift │ │ ├── Attachment.swift │ │ ├── Building.swift │ │ ├── Conference.swift │ │ ├── Day.swift │ │ ├── Event.swift │ │ ├── Formatters.swift │ │ ├── HTMLParser.swift │ │ ├── Link.swift │ │ ├── Migrations │ │ │ ├── CreateEventsSearchTable.swift │ │ │ ├── CreateEventsTable.swift │ │ │ ├── CreateParticipationsTable.swift │ │ │ ├── CreatePeopleTable.swift │ │ │ ├── CreateTracksTable.swift │ │ │ └── Migrations.swift │ │ ├── MoreItem.swift │ │ ├── MoreSection.swift │ │ ├── Participation.swift │ │ ├── Person.swift │ │ ├── Queries │ │ │ ├── GetAllTracks.swift │ │ │ ├── GetEventsByIdentifiers.swift │ │ │ ├── GetEventsBySearch.swift │ │ │ ├── GetEventsByTrack.swift │ │ │ ├── GetEventsStartingIn30Minutes.swift │ │ │ ├── GetTrackByName.swift │ │ │ ├── UpdateLinksForEvent.swift │ │ │ └── UpsertSchedule.swift │ │ ├── Requests │ │ │ ├── AppStoreSearchRequest.swift │ │ │ └── ScheduleRequest.swift │ │ ├── Room.swift │ │ ├── Schedule.swift │ │ ├── ScheduleXMLParser.swift │ │ ├── Tables │ │ │ ├── Event+Table.swift │ │ │ ├── Participation+Table.swift │ │ │ ├── Person+Table.swift │ │ │ └── Track+Table.swift │ │ └── Track.swift │ ├── Services │ │ ├── AcknowledgementService.swift │ │ ├── BuildingsService.swift │ │ ├── BundleService.swift │ │ ├── DebugServices.swift │ │ ├── FavoritesService.swift │ │ ├── GenerateDatabaseService.swift │ │ ├── InfoService.swift │ │ ├── LaunchService.swift │ │ ├── NavigationService.swift │ │ ├── NetworkService.swift │ │ ├── OpenService.swift │ │ ├── PersistenceService.swift │ │ ├── PlaybackService.swift │ │ ├── PreferencesService.swift │ │ ├── PreloadService.swift │ │ ├── ScheduleService.swift │ │ ├── Services.swift │ │ ├── SoonService.swift │ │ ├── TestsService.swift │ │ ├── TimeFormattingService.swift │ │ ├── TimeService.swift │ │ ├── TracksService.swift │ │ ├── UbiquitousPreferencesService.swift │ │ ├── UpdateService.swift │ │ ├── VideosService.swift │ │ └── YearsService.swift │ ├── ViewControllers │ │ ├── AcknowledgementsViewController.swift │ │ ├── BlueprintsEmptyViewController.swift │ │ ├── BlueprintsViewController.swift │ │ ├── DateViewController.swift │ │ ├── DisplayTimeZoneViewController.swift │ │ ├── EmbeddedBlueprintViewController.swift │ │ ├── ErrorViewController.swift │ │ ├── EventViewController.swift │ │ ├── EventsSearchController.swift │ │ ├── EventsViewController.swift │ │ ├── FullscreenBlueprintViewController.swift │ │ ├── MapContainerViewController.swift │ │ ├── MapViewController.swift │ │ ├── MoreViewController.swift │ │ ├── TextViewController.swift │ │ ├── TracksViewController.swift │ │ ├── TransportationViewController.swift │ │ ├── WelcomeViewController.swift │ │ └── YearsViewController.swift │ ├── Views │ │ ├── Action.swift │ │ ├── EventAdditionsItemView.swift │ │ ├── EventAttributesView.swift │ │ ├── EventMetadataView.swift │ │ ├── EventTableViewCell.swift │ │ ├── EventView.swift │ │ ├── FullscreenBlueprintsDismissalTransition.swift │ │ ├── HTMLRenderer.swift │ │ ├── LabelTableHeaderFooterView.swift │ │ ├── MapControlView.swift │ │ ├── MapControlsView.swift │ │ ├── RoundedButton.swift │ │ ├── ScrollImageView.swift │ │ ├── TableBackgroundView.swift │ │ └── TrackView.swift │ └── main.swift ├── Tests │ ├── AcknowledgementServiceTests.swift │ ├── AppStoreSearchRequestTests.swift │ ├── BuildingTests.swift │ ├── BuildingsServiceTests.swift │ ├── BundleDataLoader.swift │ ├── BundleServiceTests.swift │ ├── DateComponentsFormatterTests.swift │ ├── Event+Extensions.swift │ ├── FavoritesServiceTests.swift │ ├── InfoServiceTests.swift │ ├── LaunchServiceTests.swift │ ├── LinkTests.swift │ ├── NetworkServiceTests.swift │ ├── OpenServiceTests.swift │ ├── PersistenceServiceTests.swift │ ├── PlaybackServiceTests.swift │ ├── PreferencesServiceTests.swift │ ├── PreloadServiceTests.swift │ ├── QueriesTests.swift │ ├── Resources │ │ ├── Schedules │ │ │ ├── 2007.xml │ │ │ ├── 2008.xml │ │ │ ├── 2009.xml │ │ │ ├── 2010.xml │ │ │ ├── 2011.xml │ │ │ ├── 2012.xml │ │ │ ├── 2013.xml │ │ │ ├── 2014.xml │ │ │ ├── 2015.xml │ │ │ ├── 2016.xml │ │ │ ├── 2017.xml │ │ │ ├── 2018.xml │ │ │ ├── 2019.xml │ │ │ ├── 2020.xml │ │ │ ├── 2021.xml │ │ │ ├── 2022.xml │ │ │ ├── 2023.xml │ │ │ ├── 2024.xml │ │ │ └── 2025.xml │ │ └── results.json │ ├── ScheduleRequestTests.swift │ ├── ScheduleServiceTests.swift │ ├── ScheduleXMLParserTests.swift │ ├── SoonServiceTests.swift │ ├── TimeServiceTests.swift │ ├── TracksServiceTests.swift │ ├── UbiquitousPreferencesServiceTests.swift │ ├── UpdateServiceTests.swift │ ├── VideosServiceTests.swift │ └── YearsServiceTests.swift └── UITests │ ├── AgendaControllerTests.swift │ ├── ApplicationControllerTests.swift │ ├── EventControllerTests.swift │ ├── MapControllerTests.swift │ ├── MoreControllerTests.swift │ ├── Resources │ └── test.mp4 │ ├── ScreenshotTests.swift │ ├── SearchControllerTests.swift │ ├── TraitsTests.swift │ ├── VideoControllerTests.swift │ ├── XCTNSPredicateExpectation+Extensions.swift │ ├── XCTestCase+Extensions.swift │ ├── XCUIApplication+Extensions.swift │ ├── XCUIElement+Extensions.swift │ └── YearControllerTests.swift ├── Brewfile ├── LICENSE ├── Makefile ├── README.md ├── codecov.yml ├── project.yml └── take-screenshots /.gitattributes: -------------------------------------------------------------------------------- 1 | SnapshotTests/__Snapshots__ filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | test: 9 | runs-on: macOS-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Install brew dependencies 14 | run: brew bundle 15 | - name: Generate project 16 | run: make generate_project 17 | - name: Run unit, snapshot, and UI tests 18 | run: make test 19 | -------------------------------------------------------------------------------- /.package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "grdb.swift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/mttcrsp/GRDB.swift", 7 | "state" : { 8 | "branch" : "master", 9 | "revision" : "ebe3dc35991b7b9e3a451ffdf0acd9f00ab4ecd9" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-snapshot-testing", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", 16 | "state" : { 17 | "revision" : "c466812aa2e22898f27557e2e780d3aad7a27203", 18 | "version" : "1.8.2" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /.periphery.yml: -------------------------------------------------------------------------------- 1 | project: FOSDEM.xcodeproj 2 | schemes: 3 | - FOSDEM 4 | - GRDB-Package 5 | targets: 6 | - FOSDEM 7 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.10 2 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --header strip 2 | --ifdef no-indent 3 | --indent 2 4 | --tabwidth 2 5 | --wraparguments before-first 6 | --wrapcollections before-first 7 | --wrapparameters before-first 8 | --xcodeindentation enabled 9 | -------------------------------------------------------------------------------- /App/Mocks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Mocks/.gitkeep -------------------------------------------------------------------------------- /App/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /App/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/aw.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "aw.pdf", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/aw.imageset/aw.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Blueprints.xcassets/aw.imageset/aw.pdf -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/h1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "h1.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/h1.imageset/h1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Blueprints.xcassets/h1.imageset/h1.pdf -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/h2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "h2.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/h2.imageset/h2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Blueprints.xcassets/h2.imageset/h2.pdf -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/j.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "j.pdf", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/j.imageset/j.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Blueprints.xcassets/j.imageset/j.pdf -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/k1-1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "k1-1.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/k1-1.imageset/k1-1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Blueprints.xcassets/k1-1.imageset/k1-1.pdf -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/k1-2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "k1-2.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/k1-2.imageset/k1-2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Blueprints.xcassets/k1-2.imageset/k1-2.pdf -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/k2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "k2.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/k2.imageset/k2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Blueprints.xcassets/k2.imageset/k2.pdf -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/k3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "k3.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/k3.imageset/k3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Blueprints.xcassets/k3.imageset/k3.pdf -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/k4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "k4.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/k4.imageset/k4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Blueprints.xcassets/k4.imageset/k4.pdf -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/u.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "u.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /App/Resources/Blueprints.xcassets/u.imageset/u.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Blueprints.xcassets/u.imageset/u.pdf -------------------------------------------------------------------------------- /App/Resources/Buildings/aw.json: -------------------------------------------------------------------------------- 1 | { 2 | "blueprints": [ 3 | { 4 | "imageName": "aw", 5 | "title": "Building AW" 6 | } 7 | ], 8 | "coordinate": { 9 | "latitude": 50.812189418969865, 10 | "longitude": 4.380766007089079 11 | }, 12 | "polygon": [ 13 | { 14 | "latitude" : 50.812255515781715, 15 | "longitude" : 4.380436430890569 16 | }, 17 | { 18 | "latitude" : 50.812391946084688, 19 | "longitude" : 4.3808887180642841 20 | }, 21 | { 22 | "latitude" : 50.812385590644624, 23 | "longitude" : 4.3808937472026628 24 | }, 25 | { 26 | "latitude" : 50.812387920972753, 27 | "longitude" : 4.3809011232721957 28 | }, 29 | { 30 | "latitude" : 50.812310808235139, 31 | "longitude" : 4.380958790724975 32 | }, 33 | { 34 | "latitude" : 50.81230720681296, 35 | "longitude" : 4.3809483971724035 36 | }, 37 | { 38 | "latitude" : 50.812209968309219, 39 | "longitude" : 4.3810228284197308 40 | }, 41 | { 42 | "latitude" : 50.812213357890101, 43 | "longitude" : 4.3810338925241297 44 | }, 45 | { 46 | "latitude" : 50.812135397467671, 47 | "longitude" : 4.3810939069080916 48 | }, 49 | { 50 | "latitude" : 50.812133490825317, 51 | "longitude" : 4.3810878719420714 52 | }, 53 | { 54 | "latitude" : 50.812125652405769, 55 | "longitude" : 4.3810932363563779 56 | }, 57 | { 58 | "latitude" : 50.811986438885924, 59 | "longitude" : 4.3806424224920306 60 | }, 61 | { 62 | "latitude" : 50.811986015186278, 63 | "longitude" : 4.3806253234216399 64 | }, 65 | { 66 | "latitude" : 50.811992794380274, 67 | "longitude" : 4.380613588765641 68 | }, 69 | { 70 | "latitude" : 50.812238081859334, 71 | "longitude" : 4.3804256711704852 72 | }, 73 | { 74 | "latitude" : 50.812248050077045, 75 | "longitude" : 4.380424786041857 76 | } 77 | ], 78 | "title": "AW" 79 | } 80 | -------------------------------------------------------------------------------- /App/Resources/Buildings/k.json: -------------------------------------------------------------------------------- 1 | { 2 | "blueprints": [ 3 | { 4 | "imageName": "k1-1", 5 | "title": "Building K - Level 1 (1)" 6 | }, 7 | { 8 | "imageName": "k1-2", 9 | "title": "Building K - Level 1 (2)" 10 | }, 11 | { 12 | "imageName": "k2", 13 | "title": "Building K - Level 2" 14 | }, 15 | { 16 | "imageName": "k3", 17 | "title": "Building K - Level 3" 18 | }, 19 | { 20 | "imageName": "k4", 21 | "title": "Building K - Level 4" 22 | } 23 | ], 24 | "coordinate": { 25 | "latitude": 50.814769127784814, 26 | "longitude": 4.3817875546664595 27 | }, 28 | "polygon": [ 29 | { 30 | "latitude" : 50.814908326642239, 31 | "longitude" : 4.3814231109198261 32 | }, 33 | { 34 | "latitude" : 50.814918355151974, 35 | "longitude" : 4.3814223824716407 36 | }, 37 | { 38 | "latitude" : 50.814926097368328, 39 | "longitude" : 4.3814320605011687 40 | }, 41 | { 42 | "latitude" : 50.815039652703604, 43 | "longitude" : 4.3818036775757605 44 | }, 45 | { 46 | "latitude" : 50.815040759172945, 47 | "longitude" : 4.3818216006289674 48 | }, 49 | { 50 | "latitude" : 50.8150342026359, 51 | "longitude" : 4.3818320912565127 52 | }, 53 | { 54 | "latitude" : 50.814589306421652, 55 | "longitude" : 4.3821718191569801 56 | }, 57 | { 58 | "latitude" : 50.814578835225859, 59 | "longitude" : 4.3821729659432833 60 | }, 61 | { 62 | "latitude" : 50.814571420892861, 63 | "longitude" : 4.3821629076666682 64 | }, 65 | { 66 | "latitude" : 50.814457731405952, 67 | "longitude" : 4.381790282617203 68 | }, 69 | { 70 | "latitude" : 50.814456460374259, 71 | "longitude" : 4.3817738540986113 72 | }, 73 | { 74 | "latitude" : 50.814463451048283, 75 | "longitude" : 4.3817611136149708 76 | } 77 | ] 78 | , 79 | "title": "K" 80 | } 81 | -------------------------------------------------------------------------------- /App/Resources/FOSDEM.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.ubiquity-kvstore-identifier 6 | $(TeamIdentifierPrefix)$(CFBundleIdentifier) 7 | 8 | 9 | -------------------------------------------------------------------------------- /App/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | ITSAppUsesNonExemptEncryption 22 | 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | NSLocationWhenInUseUsageDescription 31 | The app uses your location data to display your current position within a map. Location data is never recorded and will never leave the app. 32 | UIApplicationSceneManifest 33 | 34 | UIApplicationSupportsMultipleScenes 35 | 36 | UISceneConfigurations 37 | 38 | 39 | UIBackgroundModes 40 | 41 | audio 42 | 43 | UILaunchStoryboardName 44 | LaunchScreen 45 | UIRequiredDeviceCapabilities 46 | 47 | armv7 48 | 49 | UISupportedInterfaceOrientations~ipad 50 | 51 | UIInterfaceOrientationPortrait 52 | UIInterfaceOrientationPortraitUpsideDown 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~iphone 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationLandscapeLeft 60 | UIInterfaceOrientationLandscapeRight 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /App/Resources/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /App/Resources/More.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/Resources/More.xcassets/contribute.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "contribute.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "contribute@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "contribute@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App/Resources/More.xcassets/contribute.imageset/contribute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/contribute.imageset/contribute.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/contribute.imageset/contribute@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/contribute.imageset/contribute@2x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/contribute.imageset/contribute@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/contribute.imageset/contribute@3x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/devrooms.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "devrooms.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "devrooms@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "devrooms@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App/Resources/More.xcassets/devrooms.imageset/devrooms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/devrooms.imageset/devrooms.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/devrooms.imageset/devrooms@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/devrooms.imageset/devrooms@2x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/devrooms.imageset/devrooms@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/devrooms.imageset/devrooms@3x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/document.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "acknowledgements.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "acknowledgements@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "acknowledgements@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App/Resources/More.xcassets/document.imageset/acknowledgements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/document.imageset/acknowledgements.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/document.imageset/acknowledgements@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/document.imageset/acknowledgements@2x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/document.imageset/acknowledgements@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/document.imageset/acknowledgements@3x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/history.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "history.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "history@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "history@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App/Resources/More.xcassets/history.imageset/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/history.imageset/history.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/history.imageset/history@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/history.imageset/history@2x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/history.imageset/history@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/history.imageset/history@3x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/timezone.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "timezone.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "timezone@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "timezone@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App/Resources/More.xcassets/timezone.imageset/timezone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/timezone.imageset/timezone.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/timezone.imageset/timezone@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/timezone.imageset/timezone@2x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/timezone.imageset/timezone@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/timezone.imageset/timezone@3x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/transportation.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "transportation.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "transportation@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "transportation@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App/Resources/More.xcassets/transportation.imageset/transportation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/transportation.imageset/transportation.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/transportation.imageset/transportation@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/transportation.imageset/transportation@2x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/transportation.imageset/transportation@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/transportation.imageset/transportation@3x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/video.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "video.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "video@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "video@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App/Resources/More.xcassets/video.imageset/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/video.imageset/video.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/video.imageset/video@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/video.imageset/video@2x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/video.imageset/video@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/video.imageset/video@3x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/years.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "years.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "years@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "years@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App/Resources/More.xcassets/years.imageset/years.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/years.imageset/years.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/years.imageset/years@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/years.imageset/years@2x.png -------------------------------------------------------------------------------- /App/Resources/More.xcassets/years.imageset/years@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/More.xcassets/years.imageset/years@3x.png -------------------------------------------------------------------------------- /App/Resources/More/bus-tram.html: -------------------------------------------------------------------------------- 1 |

Bus/metro/tram lines provided by the public transport company, STIB/MIVB.

2 |

Note you can hop on and buy your ticket in the transport by swiping your debit or credit card.

3 | 4 |

Tram 25

5 | 9 | 10 | 11 |

Bus 71

12 | 16 | 17 | 18 |

Bus 72

19 | 23 | 24 |

Tram 8

25 | -------------------------------------------------------------------------------- /App/Resources/More/car.html: -------------------------------------------------------------------------------- 1 |

If you come to FOSDEM by car, you can park your car on the campus. The main parking is located between Janson and the K building. See the map on this page for the exact location. The entrance to the parking is on Av. Franklin Roosevelt next to the H building. Note: There is no entrance from Av. Adolphe Buyl.

2 |

An additional parking on the campus is available next to the U building at Av. Antoine Depage. Note that this is a one-way street, only accessible from Av. Franklin Roosevelt as well.

3 |

You can park on the campus at your own risk. Do not leave any valuables in your car!

4 |

The parkings of the campus can be very busy since early morning during both days. An alternative is to leave your car in the Parking Sablon-Poelaert, from there to take the Tram 8, direction Roodebeek. Then follow the instructions above.

5 |

Be aware that most streets directly surrounding the ULB campus are in a so-called 'blue zone'. Parking in the blue zone is free, but limited to a maximum of two hours on Monday - Saturday. Use of a parking disc displaying the arrival time is mandatory here. The picture on the right shows one of the signs that may be used to indicate this blue zone. These signs are placed on the zone edges, so not repeated on every street inside the zone. Other nearby streets are in paid parking zones: check where you park your car.

6 |

As of 2018, the ULB campus Solbosch is within the Brussels Low Emissions Zone (LEZ). Drivers of cars registered abroad are required to register before entering the LEZ, or risk being fined. Registering is free of charge. More information can be found here.

-------------------------------------------------------------------------------- /App/Resources/More/devrooms.html: -------------------------------------------------------------------------------- 1 |

The FOSDEM team feels it is very important for free and open source software developers around the world to be able to meet in “real life”.

2 | 3 |

To this end, we have set up developer rooms (devrooms) with network/internet connectivity and projectors where teams can meet and showcase their projects. Devrooms are a place for teams to discuss, hack and publicly present latest directions, lightning talks, news and discussions. We believe developers can benefit a lot from these meetings.

4 | -------------------------------------------------------------------------------- /App/Resources/More/history.html: -------------------------------------------------------------------------------- 1 |

In 2000, Raphael Bauduin, a fan of the Linux movement in Belgium, decided to organise a small meeting for developers of Open Source software. He called it 'Open Source Developers’ European Meeting' (OSDEM).

2 | 3 |

Raphael created a mailing list, a small website and spread the word to people around him. Only a few weeks later, lots of people were waiting for an exciting event in Brussels! Invitations were sent to well-known figures in the community: Rasterman, Fyodor, Jeremy Allison and so on. They all gave a very positive response, and OSDEM was on the road to success.

4 | 5 |

For the second year, OSDEM was renamed FOSDEM. And now, many years later, it has grown into the event it is today. We now try to cover a wide spectrum of free and open source software projects, and offer a platform for people to collaborate. Every year, we host more than 5000 developers at the ULB Solbosch campus.

6 | 7 |

Raphael is no longer the driving force behind FOSDEM. After 7 years of hard work he left the team for new Open Source plans. The FOSDEM flag is now proudly carried by the following people:

8 | 9 | 43 | -------------------------------------------------------------------------------- /App/Resources/More/legal.html: -------------------------------------------------------------------------------- 1 |

The FOSDEM name and logo are trademarks of FOSDEM VZW. All content used by the application was licensed under the Creative Commons Attribution 2.0 Belgium Licence. To view a copy of this licence, visit http://creativecommons.org/licenses/by/2.0/be/deed.en or send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA. All content such as talks and biographies is the sole responsibility of the speaker.

2 | -------------------------------------------------------------------------------- /App/Resources/More/plane.html: -------------------------------------------------------------------------------- 1 |

From Brussels International Airport, located in Zaventem (about 50 minutes):

2 | -------------------------------------------------------------------------------- /App/Resources/More/taxi.html: -------------------------------------------------------------------------------- 1 |

If you need a taxi, we suggest calling Taxi Verts on +32 2 349 49 49. The address of the venue is:

2 | ULB Campus Solbosch
3 | 50, Av. Franklin D. Roosevelt
4 | 1050 Bruxelles

5 | The location where taxis expect to pick you up is marked on the map.
6 |

-------------------------------------------------------------------------------- /App/Resources/More/train.html: -------------------------------------------------------------------------------- 1 | From Brussels South(a.k.a. "Bruxelles Midi", "Brussel Zuid" or "Gare du Midi") Station (about 30 minutes):
2 | 7 | From Brussels Central("Bruxelles Central", "Brussel Centraal" or "Gare Centrale") station (about 20 minutes):
8 | 11 | From Brussels North("Bruxelles Nord" or "Gare du Nord") station (about 30 minutes):
12 | 16 | From Etterbeek station(about 20 minutes walking):
17 | -------------------------------------------------------------------------------- /App/Resources/Search.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/Resources/Search.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logo.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "logo@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "logo@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /App/Resources/Search.xcassets/logo.imageset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Search.xcassets/logo.imageset/logo.png -------------------------------------------------------------------------------- /App/Resources/Search.xcassets/logo.imageset/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Search.xcassets/logo.imageset/logo@2x.png -------------------------------------------------------------------------------- /App/Resources/Search.xcassets/logo.imageset/logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/Search.xcassets/logo.imageset/logo@3x.png -------------------------------------------------------------------------------- /App/Resources/ULB.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ULB - Université libre de Bruxelles 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /App/Resources/acknowledgements.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "GRDB.swift", 4 | "url": "https://github.com/groue/GRDB.swift" 5 | }, 6 | { 7 | "name": "Mockolo", 8 | "url": "https://github.com/uber/mockolo" 9 | }, 10 | { 11 | "name": "Periphery", 12 | "url": "https://github.com/peripheryapp/periphery" 13 | }, 14 | { 15 | "name": "SnapshotTesting", 16 | "url": "https://github.com/pointfreeco/swift-snapshot-testing" 17 | }, 18 | { 19 | "name": "SwiftFormat", 20 | "url": "https://github.com/nicklockwood/SwiftFormat" 21 | }, 22 | { 23 | "name": "xcbeautify", 24 | "url": "https://github.com/tuist/xcbeautify" 25 | }, 26 | { 27 | "name": "XcodeGen", 28 | "url": "https://github.com/yonaskolb/XcodeGen" 29 | }, 30 | { 31 | "name": "xcparse", 32 | "url": "https://github.com/ChargePoint/xcparse" 33 | } 34 | ] -------------------------------------------------------------------------------- /App/Resources/db.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/Resources/db.sqlite -------------------------------------------------------------------------------- /App/SnapshotTests/AcknowledgementsViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import SnapshotTesting 4 | import XCTest 5 | 6 | final class AcknowledgementsViewControllerTests: XCTestCase { 7 | private let dataSource = AcknowledgementsViewControllerDataSourceMock(acknowledgements: [ 8 | .init(name: "1", url: URL(fileURLWithPath: "/1")), 9 | .init(name: "2", url: URL(fileURLWithPath: "/2")), 10 | ]) 11 | 12 | func testAppearance() throws { 13 | let acknowledgementsViewController = AcknowledgementsViewController(style: .insetGrouped) 14 | acknowledgementsViewController.dataSource = dataSource 15 | assertSnapshot(matching: acknowledgementsViewController, as: .image(on: .iPhone8Plus)) 16 | } 17 | 18 | func testEvents() throws { 19 | let delegate = AcknowledgementsViewControllerDelegateMock() 20 | 21 | let acknowledgementsViewController = AcknowledgementsViewController() 22 | acknowledgementsViewController.dataSource = dataSource 23 | acknowledgementsViewController.delegate = delegate 24 | 25 | let tableView = try XCTUnwrap(acknowledgementsViewController.tableView) 26 | for section in 0 ..< tableView.numberOfSections { 27 | for row in 0 ..< tableView.numberOfRows(inSection: section) { 28 | let indexPath = IndexPath(row: row, section: section) 29 | tableView.delegate?.tableView?(tableView, didSelectRowAt: indexPath) 30 | } 31 | } 32 | 33 | XCTAssertEqual(delegate.acknowledgementsViewControllerCallCount, 2) 34 | XCTAssertEqual( 35 | delegate.acknowledgementsViewControllerArgValues.map(\.0), 36 | [acknowledgementsViewController, acknowledgementsViewController] 37 | ) 38 | XCTAssertEqual( 39 | delegate.acknowledgementsViewControllerArgValues.map(\.1), 40 | [dataSource.acknowledgements[0], dataSource.acknowledgements[1]] 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /App/SnapshotTests/BlueprintViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import SnapshotTesting 4 | import XCTest 5 | 6 | final class BlueprintViewControllerTests: XCTestCase { 7 | @available(iOS 12.0, *) 8 | func testFullscreen() throws { 9 | let building = try makeBuilding() 10 | let blueprintViewController = FullscreenBlueprintViewController() 11 | blueprintViewController.blueprint = building.blueprints[0] 12 | assertSnapshot(matching: blueprintViewController, as: .image(on: .iPhone8Plus)) 13 | 14 | blueprintViewController.blueprint = nil 15 | assertSnapshot(matching: blueprintViewController, as: .image(on: .iPhone8Plus)) 16 | 17 | blueprintViewController.blueprint = building.blueprints[1] 18 | assertSnapshot(matching: blueprintViewController, as: .image(on: .iPhone8Plus)) 19 | } 20 | 21 | @available(iOS 12.0, *) 22 | func testEmbedded() throws { 23 | let building = try makeBuilding() 24 | let blueprintViewController = EmbeddedBlueprintViewController() 25 | blueprintViewController.blueprint = building.blueprints[0] 26 | 27 | let size = CGSize(width: 300, height: 200) 28 | assertSnapshot(matching: blueprintViewController, as: .image(size: size)) 29 | 30 | blueprintViewController.blueprint = nil 31 | assertSnapshot(matching: blueprintViewController, as: .image(size: size)) 32 | 33 | blueprintViewController.blueprint = building.blueprints[1] 34 | assertSnapshot(matching: blueprintViewController, as: .image(size: size)) 35 | } 36 | 37 | func testEmpty() { 38 | let emptyViewController = BlueprintsEmptyViewController() 39 | assertSnapshot(matching: emptyViewController, as: .image(on: .iPhone8Plus)) 40 | } 41 | 42 | private func makeBuilding() throws -> Building { 43 | let json = #"{"blueprints": [{"imageName": "k1-1", "title": "Building K - Level 1 (1)"}, {"imageName": "k1-2", "title": "Building K - Level 1 (2)"}, {"imageName": "k2", "title": "Building K - Level 2"}, {"imageName": "k3", "title": "Building K - Level 3"}, {"imageName": "k4", "title": "Building K - Level 4"} ], "coordinate": {"latitude": 50.81473311542874, "longitude": 4.381869697304779 }, "polygon": [{"latitude": 50.81445648728004, "longitude": 4.381859249480868 }, {"latitude": 50.81456833802892, "longitude": 4.3822159833125625 }, {"latitude": 50.8150089595851, "longitude": 4.381883389364191 }, {"latitude": 50.814900499280014, "longitude": 4.381518608904713 } ], "title": "K"}"# 44 | return try JSONDecoder().decode(Building.self, from: Data(json.utf8)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /App/SnapshotTests/ErrorViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import SnapshotTesting 4 | import XCTest 5 | 6 | final class ErrorViewControllerTests: XCTestCase { 7 | func testAppearance() throws { 8 | let errorViewController = ErrorViewController() 9 | errorViewController.view.tintColor = .label 10 | assertSnapshot(matching: errorViewController, as: .image(on: .iPhone8Plus)) 11 | 12 | errorViewController.showsAppStoreButton = true 13 | assertSnapshot(matching: errorViewController, as: .image(on: .iPhone8Plus)) 14 | XCTAssertTrue(errorViewController.showsAppStoreButton) 15 | } 16 | 17 | func testEvents() { 18 | let delegate = ErrorViewControllerDelegateMock() 19 | 20 | let errorViewController = ErrorViewController() 21 | errorViewController.delegate = delegate 22 | 23 | let appStoreButton = errorViewController.view.findSubview(ofType: UIControl.self, accessibilityIdentifier: "appstore") 24 | appStoreButton?.sendActions(for: .touchUpInside) 25 | 26 | XCTAssertEqual(delegate.errorViewControllerDidTapAppStoreArgValues, [errorViewController]) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /App/SnapshotTests/Event+Extensions.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import Foundation 4 | 5 | extension Event { 6 | static func from(_ string: String) throws -> Event { 7 | try JSONDecoder().decode(Event.self, from: Data(string.utf8)) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /App/SnapshotTests/MoreViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import SnapshotTesting 4 | import XCTest 5 | 6 | final class MoreViewControllerTests: XCTestCase { 7 | func testAppearance() throws { 8 | let moreViewController = MoreViewController(style: .insetGrouped) 9 | moreViewController.view.tintColor = .label 10 | assertSnapshot(matching: moreViewController, as: .image(on: .iPhone8Plus)) 11 | } 12 | 13 | func testEvents() throws { 14 | let delegate = MoreViewControllerDelegateMock() 15 | 16 | let moreViewController = MoreViewController() 17 | moreViewController.delegate = delegate 18 | 19 | let tableView = try XCTUnwrap(moreViewController.tableView) 20 | for section in 0 ..< tableView.numberOfSections { 21 | for row in 0 ..< tableView.numberOfRows(inSection: section) { 22 | let indexPath = IndexPath(row: row, section: section) 23 | tableView.delegate?.tableView?(tableView, didSelectRowAt: indexPath) 24 | } 25 | } 26 | 27 | XCTAssertEqual(delegate.moreViewControllerCallCount, 10) 28 | XCTAssertEqual( 29 | delegate.moreViewControllerArgValues.map(\.0), 30 | [MoreViewController](repeating: moreViewController, count: 10) 31 | ) 32 | XCTAssertEqual( 33 | delegate.moreViewControllerArgValues.map(\.1), 34 | [.years, .history, .devrooms, .transportation, .video, .code, .acknowledgements, .legal, .overrideTime, .generateDatabase] 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /App/SnapshotTests/TextViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import SnapshotTesting 4 | import XCTest 5 | 6 | final class TextViewControllerTests: XCTestCase { 7 | func testAppearance() throws { 8 | let textViewController = TextViewController() 9 | textViewController.view.tintColor = .label 10 | textViewController.attributedText = NSAttributedString(string: "something") 11 | assertSnapshot(matching: textViewController, as: .image(on: .iPhone8Plus)) 12 | } 13 | 14 | func testAccessibilityIdentifier() { 15 | let textViewController = TextViewController() 16 | textViewController.view.tintColor = .label 17 | textViewController.attributedText = NSAttributedString(string: "something") 18 | textViewController.accessibilityIdentifier = "1" 19 | 20 | var textView = textViewController.view.findSubview(ofType: UIView.self, accessibilityIdentifier: "1") 21 | XCTAssertNotNil(textView) 22 | 23 | textViewController.accessibilityIdentifier = "2" 24 | textView = textViewController.view.findSubview(ofType: UIView.self, accessibilityIdentifier: "2") 25 | XCTAssertNotNil(textView) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /App/SnapshotTests/TransportationViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import SnapshotTesting 4 | import XCTest 5 | 6 | final class TransportationViewControllerTests: XCTestCase { 7 | func testAppearance() throws { 8 | let transportationViewController = TransportationViewController(style: .insetGrouped) 9 | assertSnapshot(matching: transportationViewController, as: .image(on: .iPhone8Plus)) 10 | } 11 | 12 | func testEvents() throws { 13 | let delegate = TransportationViewControllerDelegateMock() 14 | 15 | let transportationViewController = TransportationViewController() 16 | transportationViewController.delegate = delegate 17 | 18 | let tableView = try XCTUnwrap(transportationViewController.tableView) 19 | for section in 0 ..< tableView.numberOfSections { 20 | for row in 0 ..< tableView.numberOfRows(inSection: section) { 21 | let indexPath = IndexPath(row: row, section: section) 22 | tableView.delegate?.tableView?(tableView, didSelectRowAt: indexPath) 23 | } 24 | } 25 | 26 | XCTAssertEqual(delegate.transportationViewControllerCallCount, 7) 27 | XCTAssertEqual( 28 | delegate.transportationViewControllerArgValues.map(\.0), 29 | [TransportationViewController](repeating: transportationViewController, count: 8) 30 | ) 31 | XCTAssertEqual( 32 | delegate.transportationViewControllerArgValues.map(\.1), 33 | [.appleMaps, .googleMaps, .bus, .train, .car, .plane, .taxi] 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /App/SnapshotTests/UIView+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | func findSubview(ofType type: Subview.Type, accessibilityIdentifier: String) -> Subview? { 5 | findSubview(ofType: type, matching: { subview in subview.accessibilityIdentifier == accessibilityIdentifier }) 6 | } 7 | 8 | func findSubview(ofType type: Subview.Type, accessibilityLabel: String) -> Subview? { 9 | findSubview(ofType: type, matching: { subview in subview.accessibilityLabel == accessibilityLabel }) 10 | } 11 | 12 | func findSubview(ofType _: Subview.Type, matching predicate: (Subview) -> Bool = { _ in true }) -> Subview? { 13 | var unvisited: Set = [self] 14 | 15 | while let subview = unvisited.first { 16 | unvisited.removeFirst() 17 | unvisited.formUnion(subview.subviews) 18 | 19 | if let subview = subview as? Subview, predicate(subview) { 20 | return subview 21 | } 22 | } 23 | 24 | return nil 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /App/SnapshotTests/UIViewController+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIViewController { 4 | func findChild(ofType _: Child.Type) -> Child? { 5 | var unvisited: Set = [self] 6 | 7 | while let child = unvisited.first { 8 | unvisited.removeFirst() 9 | unvisited.formUnion(child.children) 10 | 11 | if let child = child as? Child { 12 | return child 13 | } 14 | } 15 | 16 | return nil 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /App/SnapshotTests/UIViewControllerTransitionCoordinatorMock.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class UIViewControllerTransitionCoordinatorMock: NSObject, UIViewControllerTransitionCoordinator { 4 | var isAnimated: Bool = false 5 | var presentationStyle: UIModalPresentationStyle = .fullScreen 6 | var initiallyInteractive: Bool = false 7 | var isInterruptible: Bool = false 8 | var isInteractive: Bool = false 9 | var isCancelled: Bool = false 10 | var transitionDuration: TimeInterval = 0 11 | var percentComplete: CGFloat = 0 12 | var completionVelocity: CGFloat = 0 13 | var completionCurve: UIView.AnimationCurve = .linear 14 | 15 | func animate(alongsideTransition _: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion _: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool { false } 16 | func animateAlongsideTransition(in _: UIView?, animation _: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion _: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool { false } 17 | 18 | func notifyWhenInteractionEnds(_: @escaping (UIViewControllerTransitionCoordinatorContext) -> Void) {} 19 | func notifyWhenInteractionChanges(_: @escaping (UIViewControllerTransitionCoordinatorContext) -> Void) {} 20 | 21 | func viewController(forKey _: UITransitionContextViewControllerKey) -> UIViewController? { nil } 22 | func view(forKey _: UITransitionContextViewKey) -> UIView? { nil } 23 | 24 | var containerView = UIView() 25 | var targetTransform: CGAffineTransform = .identity 26 | } 27 | -------------------------------------------------------------------------------- /App/SnapshotTests/WelcomeViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import SnapshotTesting 4 | import XCTest 5 | 6 | final class WelcomeViewControllerTests: XCTestCase { 7 | func testAppearance() throws { 8 | let welcomeViewController = WelcomeViewController(year: 2021) 9 | welcomeViewController.view.tintColor = .label 10 | assertSnapshot(matching: welcomeViewController, as: .image(on: .iPhone8Plus)) 11 | 12 | welcomeViewController.showsContinue = true 13 | assertSnapshot(matching: welcomeViewController, as: .image(on: .iPhone8Plus)) 14 | 15 | welcomeViewController.showsContinue = false 16 | assertSnapshot(matching: welcomeViewController, as: .image(on: .iPhone8Plus)) 17 | } 18 | 19 | func testEvents() throws { 20 | let delegate = WelcomeViewControllerDelegateMock() 21 | 22 | let welcomeViewController = WelcomeViewController(year: 2021) 23 | welcomeViewController.showsContinue = true 24 | welcomeViewController.delegate = delegate 25 | 26 | let continueButton = welcomeViewController.view.findSubview(ofType: UIButton.self, accessibilityIdentifier: "continue") 27 | continueButton?.sendActions(for: .touchUpInside) 28 | 29 | XCTAssertEqual(delegate.welcomeViewControllerDidTapContinueArgValues, [welcomeViewController]) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/AcknowledgementsViewControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/AcknowledgementsViewControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testEmbedded.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testEmbedded.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testEmbedded.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testEmbedded.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testEmbedded.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testEmbedded.3.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testEmpty.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testEmpty.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testFullscreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testFullscreen.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testFullscreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testFullscreen.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testFullscreen.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/BlueprintViewControllerTests/testFullscreen.3.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/ErrorViewControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/ErrorViewControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/ErrorViewControllerTests/testAppearance.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/ErrorViewControllerTests/testAppearance.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventControllerTests/testAppearance.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventControllerTests/testAppearance.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventControllerTests/testAppearance.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventControllerTests/testAppearance.3.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventControllerTests/testAppearance.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventControllerTests/testAppearance.4.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventControllerTests/testAppearance.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventControllerTests/testAppearance.5.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventControllerTests/testFavoriteEvents.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventControllerTests/testFavoriteEvents.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventControllerTests/testFavoriteNotification.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventControllerTests/testFavoriteNotification.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventControllerTests/testFavoriteNotification.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventControllerTests/testFavoriteNotification.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventControllerTests/testFavoriteNotification.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventControllerTests/testFavoriteNotification.3.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventControllerTests/testPlaybackPositionUpdatesUI.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventControllerTests/testPlaybackPositionUpdatesUI.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventControllerTests/testPlaybackPositionUpdatesUI.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventControllerTests/testPlaybackPositionUpdatesUI.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventControllerTests/testPlaybackPositionUpdatesUI.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventControllerTests/testPlaybackPositionUpdatesUI.3.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventViewControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventViewControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventViewControllerTests/testAppearance.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventViewControllerTests/testAppearance.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventViewControllerTests/testAppearance.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventViewControllerTests/testAppearance.3.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventViewControllerTests/testAppearance.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventViewControllerTests/testAppearance.4.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventViewControllerTests/testEvents.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventViewControllerTests/testEvents.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventViewControllerTests/testEvents.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventViewControllerTests/testEvents.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testAppearance.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testAppearance.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testLive.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testLive.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testLive.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testLive.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testLive.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testLive.3.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testUpdates.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testUpdates.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testUpdates.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testUpdates.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testUpdates.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/EventsViewControllerTests/testUpdates.3.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.3.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.4.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.5.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.6.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.7.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.8.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testAppearance.9.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testScrolling.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testScrolling.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testScrolling.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapContainerViewControllerTests/testScrolling.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapViewControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapViewControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapViewControllerTests/testAppearance.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapViewControllerTests/testAppearance.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapViewControllerTests/testAppearance.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapViewControllerTests/testAppearance.3.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapViewControllerTests/testAppearance.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapViewControllerTests/testAppearance.4.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MapViewControllerTests/testAppearance.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MapViewControllerTests/testAppearance.5.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/MoreViewControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/MoreViewControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/TextViewControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/TextViewControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testReloadData.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testReloadData.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testReloadData.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testReloadData.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testScroll.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testScroll.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testUpdates.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testUpdates.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testUpdates.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testUpdates.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testUpdates.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testUpdates.3.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testUpdates.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/TracksViewControllerTests/testUpdates.4.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/TransportationController/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/TransportationController/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/TransportationController/testEvents.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/TransportationController/testEvents.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/TransportationViewControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/TransportationViewControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/WelcomeViewControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/WelcomeViewControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/WelcomeViewControllerTests/testAppearance.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/WelcomeViewControllerTests/testAppearance.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/WelcomeViewControllerTests/testAppearance.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/WelcomeViewControllerTests/testAppearance.3.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/YearsViewControllerTests/testAppearance.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/YearsViewControllerTests/testAppearance.1.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/YearsViewControllerTests/testAppearance.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/YearsViewControllerTests/testAppearance.2.png -------------------------------------------------------------------------------- /App/SnapshotTests/__Snapshots__/YearsViewControllerTests/testAppearance.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/SnapshotTests/__Snapshots__/YearsViewControllerTests/testAppearance.3.png -------------------------------------------------------------------------------- /App/Sources/App.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class App: UIApplication {} 4 | -------------------------------------------------------------------------------- /App/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class AppDelegate: UIResponder, UIApplicationDelegate { 4 | var window: UIWindow? 5 | 6 | private var applicationController: ApplicationController? { 7 | window?.rootViewController as? ApplicationController 8 | } 9 | 10 | func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { 11 | let rootViewController: UIViewController 12 | do { 13 | #if DEBUG 14 | let services = try DebugServices() 15 | #else 16 | let services = try Services() 17 | #endif 18 | rootViewController = ApplicationController(dependencies: services) 19 | } catch { 20 | let errorViewController = ErrorViewController() 21 | errorViewController.showsAppStoreButton = true 22 | errorViewController.delegate = self 23 | rootViewController = errorViewController 24 | } 25 | 26 | let window = UIWindow() 27 | window.tintColor = .label 28 | window.rootViewController = rootViewController 29 | window.makeKeyAndVisible() 30 | self.window = window 31 | 32 | return true 33 | } 34 | 35 | func applicationDidBecomeActive(_: UIApplication) { 36 | applicationController?.applicationDidBecomeActive() 37 | } 38 | 39 | func applicationWillResignActive(_: UIApplication) { 40 | applicationController?.applicationWillResignActive() 41 | } 42 | } 43 | 44 | extension AppDelegate: ErrorViewControllerDelegate { 45 | func errorViewControllerDidTapAppStore(_: ErrorViewController) { 46 | if let url = URL.fosdemAppStore { 47 | UIApplication.shared.open(url) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /App/Sources/Controllers/InfoController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class InfoController: TextViewController { 4 | typealias Dependencies = HasInfoService 5 | 6 | private let dependencies: Dependencies 7 | private let info: Info 8 | 9 | init(info: Info, dependencies: Dependencies) { 10 | self.info = info 11 | self.dependencies = dependencies 12 | super.init(nibName: nil, bundle: nil) 13 | } 14 | 15 | @available(*, unavailable) 16 | required init?(coder _: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | 20 | func load(_ completion: @escaping (Error?) -> Void) { 21 | dependencies.infoService.loadAttributedText(for: info) { result in 22 | DispatchQueue.main.async { [weak self] in 23 | guard let self else { return } 24 | 25 | switch result { 26 | case let .success(attributedText): 27 | self.attributedText = attributedText 28 | completion(nil) 29 | case let .failure(error): 30 | completion(error) 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /App/Sources/Extensions/AVAudioSession+Extensions.swift: -------------------------------------------------------------------------------- 1 | import AVKit 2 | 3 | /// @mockable 4 | protocol AVAudioSessionProtocol { 5 | func setCategory(_ category: AVAudioSession.Category) throws 6 | func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws 7 | } 8 | 9 | extension AVAudioSession: AVAudioSessionProtocol {} 10 | -------------------------------------------------------------------------------- /App/Sources/Extensions/Bundle+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Bundle { 4 | var bundleShortVersion: String? { 5 | object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /App/Sources/Extensions/Calendar+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Calendar { 4 | static let gregorian = Calendar(identifier: .gregorian) 5 | } 6 | -------------------------------------------------------------------------------- /App/Sources/Extensions/MKAnnotationView+Extensions.swift: -------------------------------------------------------------------------------- 1 | import MapKit 2 | 3 | extension MKAnnotationView { 4 | static var reuseIdentifier: String { 5 | String(describing: self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /App/Sources/Extensions/TimeZone+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension TimeZone { 4 | static let conference = TimeZone(identifier: "Europe/Brussels")! 5 | } 6 | -------------------------------------------------------------------------------- /App/Sources/Extensions/UIAlertController+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIAlertController { 4 | static func makeErrorController(withHandler handler: (() -> Void)? = nil) -> UIAlertController { 5 | let dismissTitle = L10n.Error.dismiss 6 | let dismissAction = UIAlertAction(title: dismissTitle, style: .default) { _ in handler?() } 7 | 8 | let title = L10n.Error.Unknown.title, message = L10n.Error.Unknown.message 9 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) 10 | alertController.addAction(dismissAction) 11 | return alertController 12 | } 13 | 14 | static func makeNoInternetController(withRetryHandler handler: @escaping () -> Void) -> UIAlertController { 15 | let dismissTitle = L10n.Error.dismiss 16 | let dismissAction = UIAlertAction(title: dismissTitle, style: .cancel) 17 | 18 | let retryTitle = L10n.Error.retry 19 | let retryAction = UIAlertAction(title: retryTitle, style: .default) { _ in handler() } 20 | 21 | let title = L10n.Error.Internet.title, message = L10n.Error.Internet.message 22 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) 23 | alertController.addAction(retryAction) 24 | alertController.addAction(dismissAction) 25 | return alertController 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /App/Sources/Extensions/UIFont+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIFont { 4 | class func fos_preferredFont(forTextStyle style: UIFont.TextStyle, withSymbolicTraits traits: UIFontDescriptor.SymbolicTraits = []) -> UIFont { 5 | let descriptorOriginal = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style) 6 | let descriptor = descriptorOriginal.withSymbolicTraits(traits) ?? descriptorOriginal 7 | let font = UIFont(descriptor: descriptor, size: 0) 8 | return UIFontMetrics.default.scaledFont(for: font) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /App/Sources/Extensions/UIImage+Extensions.swift: -------------------------------------------------------------------------------- 1 | import CoreImage 2 | import UIKit 3 | 4 | extension UIImage { 5 | var inverted: UIImage? { 6 | guard let ciImage = CIImage(image: self) else { return nil } 7 | 8 | let filter = CIFilter(name: "CIColorInvert") 9 | filter?.setDefaults() 10 | filter?.setValue(ciImage, forKey: kCIInputImageKey) 11 | 12 | let context = CIContext(options: nil) 13 | if let output = filter?.outputImage, let copy = context.createCGImage(output, from: output.extent) { 14 | return UIImage(cgImage: copy, scale: scale, orientation: .up) 15 | } else { 16 | return nil 17 | } 18 | } 19 | } 20 | 21 | extension UIImage { 22 | static let filter: UIImage? = { 23 | if #available(iOS 15.0, *) { 24 | UIImage(systemName: "line.3.horizontal.decrease") 25 | } else { 26 | UIImage(systemName: "line.horizontal.3.decrease") 27 | } 28 | }() 29 | } 30 | -------------------------------------------------------------------------------- /App/Sources/Extensions/UITableViewCell+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UITableViewCell { 4 | static var reuseIdentifier: String { 5 | String(describing: self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /App/Sources/Extensions/UITableViewController+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UITableViewController { 4 | func deselectSelectedRow(animated: Bool) { 5 | if let indexPathForSelectedRow = tableView.indexPathForSelectedRow { 6 | tableView.deselectRow(at: indexPathForSelectedRow, animated: animated) 7 | } 8 | } 9 | } 10 | 11 | extension UITableViewController { 12 | func addSearchViewController(_ searchController: UISearchController) { 13 | navigationItem.searchController = searchController 14 | navigationItem.hidesSearchBarWhenScrolling = false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /App/Sources/Extensions/UITableViewHeaderFooterView+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UITableViewHeaderFooterView { 4 | static var reuseIdentifier: String { 5 | String(describing: self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /App/Sources/Extensions/UITraitCollection+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UITraitCollection { 4 | var fos_hasRegularSizeClasses: Bool { 5 | horizontalSizeClass == .regular && verticalSizeClass == .regular 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /App/Sources/Extensions/URL+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URL { 4 | static var fosdemAppStore: URL? { 5 | URL(string: "https://itunes.apple.com/it/app/id1513719757") 6 | } 7 | 8 | static var fosdemGithub: URL? { 9 | URL(string: "https://www.github.com/mttcrsp/fosdem") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Sources/Models/Acknowledgement.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Acknowledgement: Codable, Equatable { 4 | let name: String 5 | let url: URL 6 | } 7 | -------------------------------------------------------------------------------- /App/Sources/Models/AppStoreSearchResponse.swift: -------------------------------------------------------------------------------- 1 | struct AppStoreSearchResponse: Equatable, Decodable { 2 | let results: [AppStoreSearchResult] 3 | } 4 | -------------------------------------------------------------------------------- /App/Sources/Models/AppStoreSearchResult.swift: -------------------------------------------------------------------------------- 1 | struct AppStoreSearchResult: Equatable, Decodable { 2 | let bundleIdentifier: String 3 | let version: String 4 | 5 | enum CodingKeys: String, CodingKey { 6 | case bundleIdentifier = "bundleId" 7 | case version 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /App/Sources/Models/Attachment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum AttachmentType: String, Equatable, Codable { 4 | case slides, audio, video, paper, other 5 | } 6 | 7 | struct Attachment: Equatable, Codable { 8 | let type: AttachmentType, url: URL, name: String? 9 | } 10 | -------------------------------------------------------------------------------- /App/Sources/Models/Building.swift: -------------------------------------------------------------------------------- 1 | import MapKit 2 | 3 | struct Blueprint: Equatable, Decodable { 4 | let title: String 5 | let imageName: String 6 | } 7 | 8 | class Building: NSObject, Decodable, MKAnnotation { 9 | let glyph: String 10 | let title: String? 11 | let polygon: MKPolygon 12 | let blueprints: [Blueprint] 13 | let coordinate: CLLocationCoordinate2D 14 | 15 | required init(from decoder: Decoder) throws { 16 | let container = try decoder.container(keyedBy: CodingKeys.self) 17 | blueprints = try container.decode([Blueprint].self, forKey: .blueprints) 18 | 19 | let title = try container.decode(String.self, forKey: .title) 20 | self.title = title 21 | 22 | let glyph = try container.decodeIfPresent(String.self, forKey: .glyph) 23 | self.glyph = glyph ?? title 24 | 25 | let coordinate = try container.decode(Coordinate.self, forKey: .coordinate) 26 | self.coordinate = CLLocationCoordinate2D(coordinate: coordinate) 27 | 28 | let coordinates = try container.decode([Coordinate].self, forKey: .polygon) 29 | var coordinatesCL = coordinates.map(CLLocationCoordinate2D.init) 30 | polygon = MKPolygon(coordinates: &coordinatesCL, count: coordinatesCL.count) 31 | } 32 | 33 | private enum CodingKeys: String, CodingKey { 34 | case glyph, title, polygon, blueprints, coordinate 35 | } 36 | } 37 | 38 | private struct Coordinate: Decodable { 39 | let latitude, longitude: Double 40 | } 41 | 42 | private extension CLLocationCoordinate2D { 43 | init(coordinate: Coordinate) { 44 | self.init(latitude: coordinate.latitude, longitude: coordinate.longitude) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /App/Sources/Models/Conference.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Conference: Codable { 4 | let title: String 5 | let subtitle: String? 6 | let venue: String 7 | let city: String? 8 | let start: Date 9 | let end: Date 10 | } 11 | -------------------------------------------------------------------------------- /App/Sources/Models/Day.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Day: Codable { 4 | let index: Int, date: Date, events: [Event] 5 | } 6 | -------------------------------------------------------------------------------- /App/Sources/Models/Event.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Event: Codable { 4 | let id: Int 5 | let room: String 6 | let track: String 7 | 8 | let title: String 9 | let summary: String? 10 | let subtitle: String? 11 | let abstract: String? 12 | 13 | let date: Date 14 | let start: DateComponents 15 | let duration: DateComponents 16 | 17 | let links: [Link] 18 | let people: [Person] 19 | let attachments: [Attachment] 20 | } 21 | 22 | extension Event { 23 | var video: Link? { 24 | links.first { link in link.isMP4Video } 25 | } 26 | 27 | func isLive(at timestamp: Date) -> Bool { 28 | hasStarted(by: timestamp) && !hasEnded(by: timestamp) 29 | } 30 | 31 | func hasStarted(by timestamp: Date) -> Bool { 32 | let lowerbound = date 33 | return timestamp >= lowerbound 34 | } 35 | 36 | func hasEnded(by timestamp: Date) -> Bool { 37 | let upperbound = Calendar.gregorian.date(byAdding: duration, to: date) ?? .distantPast 38 | return timestamp >= upperbound 39 | } 40 | } 41 | 42 | extension Event: Hashable, Equatable { 43 | static func == (lhs: Event, rhs: Event) -> Bool { 44 | lhs.id == rhs.id 45 | } 46 | 47 | func hash(into hasher: inout Hasher) { 48 | hasher.combine(id) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /App/Sources/Models/Formatters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class TrackFormatter { 4 | func formattedName(from name: String) -> String { 5 | let devroomSuffix = " devroom" 6 | if name.hasSuffix(devroomSuffix) { 7 | return String(name[...name.index(name.endIndex, offsetBy: -devroomSuffix.count)]) 8 | } else { 9 | return name 10 | } 11 | } 12 | } 13 | 14 | final class SummaryFormatter { 15 | func formattedSummary(from summary: String) -> String { 16 | var summary = summary 17 | summary = summary.replacingOccurrences(of: "\t", with: " ") 18 | summary = summary.replacingOccurrences(of: "\n", with: "\n\n") 19 | return summary 20 | } 21 | } 22 | 23 | final class PeopleFormatter { 24 | func formattedPeople(from people: [Person]) -> String { 25 | people.map { person in person.name }.joined(separator: ", ") 26 | } 27 | } 28 | 29 | final class DurationFormatter { 30 | func duration(from duration: DateComponents) -> String? { 31 | Self.formatter.string(from: duration) 32 | } 33 | 34 | private static let formatter: DateComponentsFormatter = { 35 | let formatter = DateComponentsFormatter() 36 | formatter.allowedUnits = [.minute] 37 | return formatter 38 | }() 39 | } 40 | -------------------------------------------------------------------------------- /App/Sources/Models/HTMLParser.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | struct HTMLNode { 4 | var content: String? 5 | var name: String? 6 | var children: [HTMLNode] = [] 7 | var properties: [String: String] = [:] 8 | } 9 | 10 | struct HTMLParser { 11 | func parse(_ string: String) -> HTMLNode? { 12 | let documentPointer = string.cString(using: .utf8)?.withUnsafeBufferPointer { pointer in 13 | let options = [HTML_PARSE_RECOVER, HTML_PARSE_NOERROR, HTML_PARSE_NOWARNING].map(\.rawValue).reduce(0, |) 14 | return htmlReadMemory(pointer.baseAddress, numericCast(pointer.count), nil, nil, Int32(options)) 15 | } 16 | 17 | if let documentPointer { 18 | defer { xmlFreeDoc(documentPointer) } 19 | return HTMLNode(documentPointer.pointee) 20 | } else { 21 | return nil 22 | } 23 | } 24 | } 25 | 26 | private extension HTMLNode { 27 | init(_ visitable: Visitable) { 28 | if let cContent = visitable.content { 29 | content = String(cString: cContent) 30 | } 31 | 32 | if let cName = visitable.name { 33 | name = String(cString: cName) 34 | } 35 | 36 | if let xmlNode = visitable as? xmlNode { 37 | var current = visitable.properties 38 | while let property = current?.pointee { 39 | if let value = withUnsafePointer(to: xmlNode, { xmlGetProp($0, property.name) }) { 40 | properties[String(cString: property.name)] = String(cString: value) 41 | } 42 | current = property.next 43 | } 44 | } 45 | 46 | var current = visitable.children 47 | while let child = current?.pointee { 48 | children.append(.init(child)) 49 | current = child.next 50 | } 51 | } 52 | } 53 | 54 | private protocol Visitable { 55 | var children: UnsafeMutablePointer<_xmlNode>! { get } 56 | var last: UnsafeMutablePointer<_xmlNode>! { get } 57 | var next: UnsafeMutablePointer<_xmlNode>! { get } 58 | var name: UnsafePointer! { get } 59 | var content: UnsafeMutablePointer! { get } 60 | var properties: UnsafeMutablePointer<_xmlAttr>! { get } 61 | } 62 | 63 | extension _xmlNode: Visitable {} 64 | extension _xmlDoc: Visitable { 65 | var name: UnsafePointer! { nil } 66 | var content: UnsafeMutablePointer! { nil } 67 | var properties: UnsafeMutablePointer<_xmlAttr>! { nil } 68 | } 69 | -------------------------------------------------------------------------------- /App/Sources/Models/Link.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Link: Equatable, Codable { 4 | let name: String, url: URL? 5 | } 6 | 7 | extension Link { 8 | var isVideo: Bool { 9 | isMP4Video || isWEBMVideo 10 | } 11 | 12 | var isMP4Video: Bool { 13 | url?.host == "video.fosdem.org" && url?.pathExtension == "mp4" 14 | } 15 | 16 | var isWEBMVideo: Bool { 17 | url?.host == "video.fosdem.org" && url?.pathExtension == "webm" 18 | } 19 | } 20 | 21 | extension Link { 22 | var isLivestream: Bool { 23 | livestreamURL != nil 24 | } 25 | 26 | var livestreamURL: URL? { 27 | if let url, url.host == "live.fosdem.org", url.pathComponents.contains("watch"), let identifier = url.pathComponents.last { 28 | URL(string: "https://stream.fosdem.org")?.appendingPathComponent(identifier).appendingPathExtension("m3u8") 29 | } else { 30 | nil 31 | } 32 | } 33 | } 34 | 35 | extension Link { 36 | var isAddition: Bool { 37 | !isFeedback && !isChat && !isLivestream && !isVideo 38 | } 39 | 40 | private var isFeedback: Bool { 41 | if let url { 42 | url.host == "submission.fosdem.org" && url.pathComponents.contains("feedback") 43 | } else { 44 | false 45 | } 46 | } 47 | 48 | private var isChat: Bool { 49 | if let url { 50 | url.absoluteString.contains("chat.fosdem.org") 51 | } else { 52 | false 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /App/Sources/Models/Migrations/CreateEventsSearchTable.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | struct CreateEventsSearchTable: PersistenceServiceMigration { 4 | let identifier = "create events search table" 5 | 6 | func perform(in database: GRDB.Database) throws { 7 | try database.create(virtualTable: Event.searchDatabaseTableName, using: FTS5()) { table in 8 | table.synchronize(withTable: Event.databaseTableName) 9 | table.tokenizer = .porter(wrapping: .ascii()) 10 | 11 | for column in Event.Columns.searchable { 12 | table.column(column.rawValue) 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /App/Sources/Models/Migrations/CreateEventsTable.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | struct CreateEventsTable: PersistenceServiceMigration { 4 | let identifier = "create events table" 5 | 6 | func perform(in database: GRDB.Database) throws { 7 | try database.create(table: Event.databaseTableName) { table in 8 | table.column(Event.Columns.id.rawValue).primaryKey(onConflict: .replace) 9 | table.column(Event.Columns.room.rawValue).notNull().indexed() 10 | table.column(Event.Columns.track.rawValue).notNull().indexed() 11 | 12 | table.column(Event.Columns.title.rawValue).notNull() 13 | table.column(Event.Columns.summary.rawValue) 14 | table.column(Event.Columns.subtitle.rawValue) 15 | table.column(Event.Columns.abstract.rawValue) 16 | 17 | table.column(Event.Columns.date.rawValue, .datetime) 18 | table.column(Event.Columns.start.rawValue).notNull() 19 | table.column(Event.Columns.duration.rawValue).notNull() 20 | 21 | table.column(Event.Columns.links.rawValue, .blob).notNull() 22 | table.column(Event.Columns.people.rawValue, .blob).notNull() 23 | table.column(Event.Columns.attachments.rawValue, .blob).notNull() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /App/Sources/Models/Migrations/CreateParticipationsTable.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | struct CreateParticipationsTable: PersistenceServiceMigration { 4 | let identifier = "create participations table" 5 | 6 | func perform(in database: Database) throws { 7 | try database.create(table: Participation.databaseTableName) { table in 8 | table.column(Participation.Columns.personID.rawValue).notNull() 9 | table.column(Participation.Columns.eventID.rawValue).notNull() 10 | table.primaryKey([ 11 | Participation.Columns.eventID.rawValue, 12 | Participation.Columns.personID.rawValue, 13 | ], onConflict: .replace) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /App/Sources/Models/Migrations/CreatePeopleTable.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | struct CreatePeopleTable: PersistenceServiceMigration { 4 | let identifier = "create people table" 5 | 6 | func perform(in database: Database) throws { 7 | try database.create(table: Person.databaseTableName) { table in 8 | table.column(Person.Columns.id.rawValue).primaryKey(onConflict: .replace) 9 | table.column(Person.Columns.name.rawValue).notNull().indexed() 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /App/Sources/Models/Migrations/CreateTracksTable.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | struct CreateTracksTable: PersistenceServiceMigration { 4 | let identifier = "create tracks table" 5 | 6 | func perform(in database: Database) throws { 7 | try database.create(table: Track.databaseTableName) { table in 8 | table.column(Track.Columns.name.rawValue).primaryKey(onConflict: .replace) 9 | table.column(Track.Columns.day.rawValue, .integer) 10 | table.column(Track.Columns.date.rawValue, .date) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /App/Sources/Models/Migrations/Migrations.swift: -------------------------------------------------------------------------------- 1 | extension [PersistenceServiceMigration] { 2 | static var allMigrations: [PersistenceServiceMigration] { 3 | [ 4 | CreateTracksTable(), 5 | CreatePeopleTable(), 6 | CreateEventsTable(), 7 | CreateEventsSearchTable(), 8 | CreateParticipationsTable(), 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Sources/Models/MoreItem.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum MoreItem: String, CaseIterable { 4 | case code 5 | case legal 6 | case years 7 | case video 8 | case history 9 | case devrooms 10 | case transportation 11 | case acknowledgements 12 | case timeZone 13 | 14 | #if DEBUG 15 | case overrideTime 16 | case generateDatabase 17 | #endif 18 | } 19 | 20 | extension MoreItem { 21 | var title: String { 22 | switch self { 23 | case .code: 24 | return L10n.Code.title 25 | case .legal: 26 | return L10n.Legal.title 27 | case .video: 28 | return L10n.Recent.video 29 | case .history: 30 | return L10n.History.title 31 | case .devrooms: 32 | return L10n.Devrooms.title 33 | case .transportation: 34 | return L10n.Transportation.title 35 | case .acknowledgements: 36 | return L10n.Acknowledgements.title 37 | case .timeZone: 38 | return L10n.TimeZone.title 39 | case .years: 40 | let lowerBound = YearsService.all.lowerBound 41 | let upperBound = YearsService.all.upperBound 42 | return L10n.Years.item(lowerBound, upperBound) 43 | #if DEBUG 44 | case .overrideTime: 45 | return "Override current time" 46 | case .generateDatabase: 47 | return "Generate database" 48 | #endif 49 | } 50 | } 51 | 52 | var icon: UIImage? { 53 | switch self { 54 | case .code: 55 | return Asset.More.contribute.image 56 | case .legal: 57 | return Asset.More.document.image 58 | case .years: 59 | return Asset.More.years.image 60 | case .video: 61 | return Asset.More.video.image 62 | case .history: 63 | return Asset.More.history.image 64 | case .devrooms: 65 | return Asset.More.devrooms.image 66 | case .transportation: 67 | return Asset.More.transportation.image 68 | case .acknowledgements: 69 | return Asset.More.document.image 70 | case .timeZone: 71 | return Asset.More.timezone.image 72 | #if DEBUG 73 | case .overrideTime, .generateDatabase: 74 | return nil 75 | #endif 76 | } 77 | } 78 | 79 | var info: Info? { 80 | switch self { 81 | case .legal: 82 | return .legal 83 | case .history: 84 | return .history 85 | case .devrooms: 86 | return .devrooms 87 | case .transportation: 88 | return .transportation 89 | case .code, .years, .video, .timeZone, .acknowledgements: 90 | return nil 91 | #if DEBUG 92 | case .overrideTime, .generateDatabase: 93 | return nil 94 | #endif 95 | } 96 | } 97 | 98 | var accessibilityIdentifier: String { 99 | rawValue 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /App/Sources/Models/MoreSection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum MoreSection: CaseIterable { 4 | case years 5 | case about 6 | case recent 7 | case settings 8 | case other 9 | #if DEBUG 10 | case debug 11 | #endif 12 | } 13 | 14 | extension MoreSection { 15 | var items: [MoreItem] { 16 | switch self { 17 | case .years: 18 | return [.years] 19 | case .recent: 20 | return [.video] 21 | case .other: 22 | return [.code, .acknowledgements, .legal] 23 | case .about: 24 | return [.history, .devrooms, .transportation] 25 | case .settings: 26 | return [.timeZone] 27 | #if DEBUG 28 | case .debug: 29 | return [.overrideTime, .generateDatabase] 30 | #endif 31 | } 32 | } 33 | 34 | var title: String? { 35 | switch self { 36 | case .years: 37 | return L10n.More.Section.years 38 | case .recent: 39 | return L10n.More.Section.recent 40 | case .about: 41 | return L10n.More.Section.about 42 | case .settings: 43 | return L10n.More.Section.settings 44 | case .other: 45 | return L10n.More.Section.other 46 | #if DEBUG 47 | case .debug: 48 | return "Debug" 49 | #endif 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /App/Sources/Models/Participation.swift: -------------------------------------------------------------------------------- 1 | struct Participation: Codable { 2 | let personID, eventID: Int 3 | } 4 | -------------------------------------------------------------------------------- /App/Sources/Models/Person.swift: -------------------------------------------------------------------------------- 1 | struct Person: Codable { 2 | let id: Int, name: String 3 | } 4 | 5 | extension Person: Equatable, Hashable { 6 | static func == (lhs: Person, rhs: Person) -> Bool { 7 | lhs.id == rhs.id 8 | } 9 | 10 | func hash(into hasher: inout Hasher) { 11 | hasher.combine(id) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /App/Sources/Models/Queries/GetAllTracks.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | struct GetAllTracks: PersistenceServiceRead, Equatable { 4 | func perform(in database: Database) throws -> [Track] { 5 | try Track 6 | .filter(!Track.Columns.name.like("% stand")) 7 | .order(Track.Columns.name.lowercased) 8 | .fetchAll(database) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /App/Sources/Models/Queries/GetEventsByIdentifiers.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | struct GetEventsByIdentifiers: PersistenceServiceRead, Equatable { 4 | let identifiers: Set 5 | 6 | func perform(in database: Database) throws -> [Event] { 7 | try Event.order([Event.Columns.date]) 8 | .filter(identifiers.contains(Event.Columns.id)) 9 | .fetchAll(database) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Sources/Models/Queries/GetEventsBySearch.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import GRDB 3 | 4 | struct GetEventsBySearch: PersistenceServiceRead { 5 | let query: String 6 | 7 | func perform(in database: Database) throws -> [Event] { 8 | let query = query.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 9 | 10 | let components = query.components(separatedBy: " ") 11 | let componentsEscaped = components.map { "\"\($0)\"" } 12 | 13 | let predicate = componentsEscaped.joined(separator: " AND ") 14 | let predicateWithPrefix = "\(predicate) *" 15 | 16 | let pattern = try database.makeFTS5Pattern(rawPattern: predicateWithPrefix, forTable: Event.searchDatabaseTableName) 17 | 18 | return try Event.fetchAll(database, sql: """ 19 | SELECT events.* 20 | FROM events JOIN events_search ON events.id = events_search.id 21 | WHERE events_search MATCH ? 22 | ORDER BY bm25(events_search, 5.0, 2.0, 5.0, 3.0, 1.0, 1.0, 3.0) 23 | LIMIT 50 24 | """, arguments: [pattern]) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /App/Sources/Models/Queries/GetEventsByTrack.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | struct GetEventsByTrack: PersistenceServiceRead { 4 | let track: String 5 | 6 | func perform(in database: Database) throws -> [Event] { 7 | try Event.fetchAll(database, sql: """ 8 | SELECT * 9 | FROM events 10 | WHERE track = ? 11 | ORDER BY date 12 | """, arguments: [track]) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /App/Sources/Models/Queries/GetEventsStartingIn30Minutes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import GRDB 3 | 4 | struct GetEventsStartingIn30Minutes: PersistenceServiceRead, Equatable { 5 | let now: Date 6 | 7 | func perform(in database: Database) throws -> [Event] { 8 | let calendar = Calendar.gregorian 9 | let upperbound = calendar.date(byAdding: .minute, value: 30, to: now) 10 | let lowerbound = now 11 | 12 | return try Event.fetchAll(database, sql: """ 13 | SELECT * 14 | FROM events 15 | WHERE events.date > ? AND events.date < ? 16 | ORDER BY date 17 | """, arguments: [lowerbound, upperbound]) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /App/Sources/Models/Queries/GetTrackByName.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | struct GetTrackByName: PersistenceServiceRead, Equatable { 4 | let name: String 5 | 6 | func perform(in database: Database) throws -> Track? { 7 | try Track 8 | .filter(key: name) 9 | .fetchOne(database) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Sources/Models/Queries/UpdateLinksForEvent.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | import Foundation 3 | import GRDB 4 | 5 | struct UpdateLinksForEvent: PersistenceServiceWrite { 6 | let eventID: Int 7 | let links: [Link] 8 | 9 | func perform(in database: Database) throws { 10 | let data = try JSONEncoder().encode(links) 11 | let sql = "UPDATE events SET links = ?, summary = 'testing' WHERE id = ?" 12 | try database.execute(sql: sql, arguments: [data, eventID]) 13 | } 14 | } 15 | #endif 16 | -------------------------------------------------------------------------------- /App/Sources/Models/Queries/UpsertSchedule.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | struct UpsertSchedule: PersistenceServiceWrite { 4 | let schedule: Schedule 5 | 6 | func perform(in database: Database) throws { 7 | for type in [Event.self, Track.self, Person.self, Participation.self] as [MutablePersistableRecord.Type] { 8 | try type.deleteAll(database) 9 | } 10 | 11 | for day in schedule.days { 12 | for event in day.events { 13 | try event.insert(database) 14 | 15 | let track = Track(name: event.track, day: day.index, date: day.date) 16 | try track.insert(database) 17 | 18 | for person in event.people { 19 | try person.insert(database) 20 | 21 | let participation = Participation(personID: person.id, eventID: event.id) 22 | try participation.insert(database) 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /App/Sources/Models/Requests/AppStoreSearchRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct AppStoreSearchRequest: NetworkRequest { 4 | var url: URL { 5 | URL(string: "https://itunes.apple.com/us/search?term=fosdem&media=software&entity=software")! 6 | } 7 | 8 | func decode(_ data: Data?, response _: HTTPURLResponse?) throws -> AppStoreSearchResponse { 9 | try JSONDecoder().decode(AppStoreSearchResponse.self, from: data ?? Data()) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Sources/Models/Requests/ScheduleRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ScheduleRequest: NetworkRequest { 4 | enum Error: CustomNSError { 5 | case notFound 6 | } 7 | 8 | let year: Int 9 | 10 | var url: URL { 11 | URL(string: "https://fosdem.org/")! 12 | .appendingPathComponent(year.description) 13 | .appendingPathComponent("schedule") 14 | .appendingPathComponent("xml") 15 | } 16 | 17 | func decode(_ data: Data?, response: HTTPURLResponse?) throws -> Schedule { 18 | guard let data, response?.statusCode != 404 else { 19 | throw Error.notFound 20 | } 21 | 22 | let parser = ScheduleXMLParser(data: data) 23 | 24 | if parser.parse(), let schedule = parser.schedule { 25 | return schedule 26 | } 27 | 28 | if let validationError = parser.validationError { 29 | throw validationError 30 | } 31 | 32 | if let parseError = parser.parseError { 33 | throw parseError 34 | } 35 | 36 | throw NSError(domain: "com.mttcrsp.ScheduleRequest", code: 1) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /App/Sources/Models/Room.swift: -------------------------------------------------------------------------------- 1 | struct Room: Codable { 2 | let name: String, events: [Event] 3 | } 4 | -------------------------------------------------------------------------------- /App/Sources/Models/Schedule.swift: -------------------------------------------------------------------------------- 1 | struct Schedule: Codable { 2 | let conference: Conference, days: [Day] 3 | } 4 | -------------------------------------------------------------------------------- /App/Sources/Models/Tables/Participation+Table.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | extension Participation: PersistableRecord, FetchableRecord { 4 | static var databaseTableName: String { 5 | "participations" 6 | } 7 | 8 | enum Columns: String, ColumnExpression { 9 | case personID, eventID 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Sources/Models/Tables/Person+Table.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | extension Person: PersistableRecord, FetchableRecord { 4 | static var databaseTableName: String { 5 | "people" 6 | } 7 | 8 | enum Columns: String, ColumnExpression { 9 | case id, name 10 | } 11 | 12 | init(row: Row) { 13 | self.init(id: row[Columns.id], name: row[Columns.name]) 14 | } 15 | 16 | func encode(to container: inout PersistenceContainer) { 17 | container[Columns.id.rawValue] = id 18 | container[Columns.name.rawValue] = name 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /App/Sources/Models/Tables/Track+Table.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | extension Track: PersistableRecord, FetchableRecord { 4 | static var databaseTableName: String { 5 | "tracks" 6 | } 7 | 8 | enum Columns: String, ColumnExpression { 9 | case name, day, date 10 | } 11 | 12 | init(row: Row) { 13 | self.init(name: row[Columns.name], day: row[Columns.day], date: row[Columns.date]) 14 | } 15 | 16 | func encode(to container: inout PersistenceContainer) { 17 | container[Columns.day.rawValue] = day 18 | container[Columns.name.rawValue] = name 19 | container[Columns.date.rawValue] = date 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /App/Sources/Models/Track.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Track: Codable { 4 | let name: String, day: Int, date: Date 5 | } 6 | 7 | extension Track: Equatable { 8 | static func == (lhs: Track, rhs: Track) -> Bool { 9 | lhs.name == rhs.name 10 | } 11 | } 12 | 13 | extension Track { 14 | var formattedName: String { 15 | TrackFormatter().formattedName(from: name) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /App/Sources/Services/AcknowledgementService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class AcknowledgementsService { 4 | enum Error: CustomNSError { 5 | case resourceNotFound 6 | } 7 | 8 | private let dataProvider: AcknowledgementsServiceDataProvider 9 | private let bundle: AcknowledgementsServiceBundle 10 | 11 | init(bundle: AcknowledgementsServiceBundle = Bundle.main, dataProvider: AcknowledgementsServiceDataProvider = AcknowledgementsServiceData()) { 12 | self.dataProvider = dataProvider 13 | self.bundle = bundle 14 | } 15 | 16 | func loadAcknowledgements() throws -> [Acknowledgement] { 17 | guard let url = bundle.url(forResource: "acknowledgements", withExtension: "json") else { 18 | throw Error.resourceNotFound 19 | } 20 | 21 | let data = try dataProvider.data(withContentsOf: url) 22 | let decoder = JSONDecoder() 23 | return try decoder.decode([Acknowledgement].self, from: data) 24 | } 25 | } 26 | 27 | /// @mockable 28 | protocol AcknowledgementsServiceProtocol { 29 | func loadAcknowledgements() throws -> [Acknowledgement] 30 | } 31 | 32 | extension AcknowledgementsService: AcknowledgementsServiceProtocol {} 33 | 34 | /// @mockable 35 | protocol AcknowledgementsServiceBundle { 36 | func url(forResource name: String?, withExtension ext: String?) -> URL? 37 | } 38 | 39 | extension Bundle: AcknowledgementsServiceBundle {} 40 | 41 | /// @mockable 42 | protocol AcknowledgementsServiceDataProvider { 43 | func data(withContentsOf url: URL) throws -> Data 44 | } 45 | 46 | final class AcknowledgementsServiceData: AcknowledgementsServiceDataProvider { 47 | func data(withContentsOf url: URL) throws -> Data { 48 | try Data(contentsOf: url) 49 | } 50 | } 51 | 52 | protocol HasAcknowledgementsService { 53 | var acknowledgementsService: AcknowledgementsServiceProtocol { get } 54 | } 55 | -------------------------------------------------------------------------------- /App/Sources/Services/BuildingsService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class BuildingsService { 4 | enum Error: CustomNSError { 5 | case missingData, partialData 6 | } 7 | 8 | private let bundleService: BuildingsServiceBundle 9 | private let queue: DispatchQueue 10 | 11 | init(bundleService: BuildingsServiceBundle, queue: DispatchQueue = .global()) { 12 | self.bundleService = bundleService 13 | self.queue = queue 14 | } 15 | 16 | func loadBuildings(completion: @escaping ([Building], Error?) -> Void) { 17 | queue.async { [weak self] in 18 | guard let self else { return } 19 | 20 | var buildings: [Building] = [] 21 | 22 | let resources = ["aw", "f", "h", "j", "k", "u", "s"] 23 | for resource in resources { 24 | do { 25 | let buildingData = try bundleService.data(forResource: resource, withExtension: "json") 26 | let building = try JSONDecoder().decode(Building.self, from: buildingData) 27 | buildings.append(building) 28 | } catch {} 29 | } 30 | 31 | switch buildings.count { 32 | case resources.count: 33 | completion(buildings, nil) 34 | case 0: 35 | completion([], .missingData) 36 | case _: 37 | completion(buildings, .partialData) 38 | } 39 | } 40 | } 41 | } 42 | 43 | /// @mockable 44 | protocol BuildingsServiceProtocol { 45 | func loadBuildings(completion: @escaping ([Building], BuildingsService.Error?) -> Void) 46 | } 47 | 48 | extension BuildingsService: BuildingsServiceProtocol {} 49 | 50 | /// @mockable 51 | protocol BuildingsServiceBundle { 52 | func data(forResource name: String?, withExtension ext: String?) throws -> Data 53 | } 54 | 55 | extension BundleService: BuildingsServiceBundle {} 56 | 57 | protocol HasBuildingsService { 58 | var buildingsService: BuildingsServiceProtocol { get } 59 | } 60 | -------------------------------------------------------------------------------- /App/Sources/Services/BundleService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class BundleService { 4 | enum Error: CustomNSError { 5 | case resourceNotFound 6 | } 7 | 8 | private let dataProvider: BundleServiceDataProvider 9 | private let bundle: BundleServiceBundle 10 | 11 | init(bundle: BundleServiceBundle = Bundle.main, dataProvider: BundleServiceDataProvider = BundleServiceData()) { 12 | self.dataProvider = dataProvider 13 | self.bundle = bundle 14 | } 15 | 16 | func data(forResource name: String?, withExtension ext: String?) throws -> Data { 17 | guard let url = bundle.url(forResource: name, withExtension: ext) else { 18 | throw Error.resourceNotFound 19 | } 20 | return try dataProvider.data(withContentsOf: url) 21 | } 22 | } 23 | 24 | /// @mockable 25 | protocol BundleServiceBundle { 26 | func url(forResource name: String?, withExtension ext: String?) -> URL? 27 | } 28 | 29 | extension Bundle: BundleServiceBundle {} 30 | 31 | /// @mockable 32 | protocol BundleServiceDataProvider { 33 | func data(withContentsOf url: URL) throws -> Data 34 | } 35 | 36 | final class BundleServiceData: BundleServiceDataProvider { 37 | func data(withContentsOf url: URL) throws -> Data { 38 | try Data(contentsOf: url) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /App/Sources/Services/DebugServices.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | final class DebugServices: Services { 3 | override init() throws { 4 | let testsService = TestsService() 5 | testsService.runPreInitializationTestCommands() 6 | try super.init() 7 | testsService.runPostInitializationTestCommands(with: self) 8 | } 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /App/Sources/Services/GenerateDatabaseService.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | import Foundation 3 | 4 | final class GenerateDatabaseService { 5 | private let fileManager: FileManager 6 | private let networkService: NetworkService 7 | 8 | init(networkService: NetworkService = .init(session: URLSession.shared), fileManager: FileManager = .default) { 9 | self.fileManager = fileManager 10 | self.networkService = networkService 11 | } 12 | 13 | func generate(forYear year: Year, completion: @escaping (Result) -> Void) { 14 | let databaseFile = fileManager.temporaryDirectory 15 | .appendingPathComponent("db") 16 | .appendingPathExtension("sqlite") 17 | 18 | networkService.perform(ScheduleRequest(year: year)) { result in 19 | switch result { 20 | case let .failure(error): 21 | completion(.failure(error)) 22 | case let .success(schedule): 23 | do { 24 | let persistenceService = try PersistenceService(path: databaseFile.path, migrations: .allMigrations) 25 | try persistenceService.performWriteSync(UpsertSchedule(schedule: schedule)) 26 | completion(.success(databaseFile)) 27 | } catch { 28 | completion(.failure(error)) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | #endif 35 | -------------------------------------------------------------------------------- /App/Sources/Services/NetworkService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol NetworkRequest { 4 | associatedtype Model 5 | 6 | var url: URL { get } 7 | var httpBody: Data? { get } 8 | var httpMethod: String { get } 9 | var allHTTPHeaderFields: [String: String]? { get } 10 | 11 | func decode(_ data: Data?, response: HTTPURLResponse?) throws -> Model 12 | } 13 | 14 | final class NetworkService { 15 | private let session: NetworkServiceSession 16 | 17 | init(session: NetworkServiceSession) { 18 | self.session = session 19 | } 20 | 21 | @discardableResult 22 | func perform(_ request: Request, completion: @escaping (Result) -> Void) -> NetworkServiceTask { 23 | let task = session.dataTask(with: request.httpRequest) { data, response, error in 24 | if let error = error as? URLError, error.code == .cancelled { 25 | return // Do nothing 26 | } else if let error { 27 | return completion(.failure(error)) 28 | } 29 | 30 | do { 31 | let model = try request.decode(data, response: response as? HTTPURLResponse) 32 | completion(.success(model)) 33 | } catch { 34 | completion(.failure(error)) 35 | } 36 | } 37 | 38 | task.resume() 39 | return task 40 | } 41 | } 42 | 43 | extension NetworkRequest { 44 | var httpBody: Data? { 45 | nil 46 | } 47 | 48 | var httpMethod: String { 49 | "GET" 50 | } 51 | 52 | var allHTTPHeaderFields: [String: String]? { 53 | nil 54 | } 55 | } 56 | 57 | private extension NetworkRequest { 58 | var httpRequest: URLRequest { 59 | let request = NSMutableURLRequest(url: url) 60 | request.allHTTPHeaderFields = allHTTPHeaderFields 61 | request.httpMethod = httpMethod 62 | request.httpBody = httpBody 63 | return request as URLRequest 64 | } 65 | } 66 | 67 | /// @mockable 68 | protocol NetworkServiceTask { 69 | func cancel() 70 | func resume() 71 | } 72 | 73 | extension URLSessionDataTask: NetworkServiceTask {} 74 | 75 | /// @mockable 76 | protocol NetworkServiceSession { 77 | func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> NetworkServiceTask 78 | } 79 | 80 | extension URLSession: NetworkServiceSession { 81 | func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> NetworkServiceTask { 82 | dataTask(with: request, completionHandler: completionHandler) as URLSessionDataTask 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /App/Sources/Services/OpenService.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class OpenService { 4 | private let application: OpenServiceApplication 5 | 6 | init(application: OpenServiceApplication = UIApplication.shared) { 7 | self.application = application 8 | } 9 | 10 | func open(_ url: URL, completion: ((Bool) -> Void)?) { 11 | application.open(url, completionHandler: completion) 12 | } 13 | } 14 | 15 | /// @mockable 16 | protocol OpenServiceProtocol { 17 | func open(_ url: URL, completion: ((Bool) -> Void)?) 18 | } 19 | 20 | extension OpenService: OpenServiceProtocol {} 21 | 22 | /// @mockable 23 | protocol OpenServiceApplication { 24 | func open(_ url: URL, completionHandler completion: ((Bool) -> Void)?) 25 | } 26 | 27 | extension UIApplication: OpenServiceApplication { 28 | func open(_ url: URL, completionHandler completion: ((Bool) -> Void)?) { 29 | open(url, options: [:], completionHandler: completion) 30 | } 31 | } 32 | 33 | protocol HasOpenService { 34 | var openService: OpenServiceProtocol { get } 35 | } 36 | -------------------------------------------------------------------------------- /App/Sources/Services/PersistenceService.swift: -------------------------------------------------------------------------------- 1 | import GRDB 2 | 3 | protocol PersistenceServiceWrite { 4 | func perform(in database: Database) throws 5 | } 6 | 7 | protocol PersistenceServiceRead { 8 | func perform(in database: Database) throws -> Model 9 | associatedtype Model 10 | } 11 | 12 | protocol PersistenceServiceMigration { 13 | func perform(in database: Database) throws 14 | var identifier: String { get } 15 | } 16 | 17 | final class PersistenceService { 18 | private let database: DatabaseQueue 19 | 20 | init(path: String?, migrations: [PersistenceServiceMigration]) throws { 21 | if let path { 22 | database = try DatabaseQueue(path: path) 23 | } else { 24 | database = try DatabaseQueue() 25 | } 26 | 27 | var migrator = DatabaseMigrator() 28 | for migration in migrations { 29 | migrator.registerMigration(migration.identifier, migrate: migration.perform) 30 | } 31 | try migrator.migrate(database) 32 | } 33 | 34 | func performWriteSync(_ write: PersistenceServiceWrite) throws { 35 | try database.write(write.perform) 36 | } 37 | 38 | func performReadSync(_ read: Read) throws -> Read.Model { 39 | try database.read(read.perform) 40 | } 41 | 42 | func performWrite(_ write: PersistenceServiceWrite, completion: @escaping (Error?) -> Void) { 43 | database.asyncWrite({ database in 44 | try write.perform(in: database) 45 | }, completion: { _, result in 46 | switch result { 47 | case .success: 48 | completion(nil) 49 | case let .failure(error): 50 | completion(error) 51 | } 52 | }) 53 | } 54 | 55 | func performRead(_ read: Read, completion: @escaping (Result) -> Void) { 56 | database.asyncRead { result in 57 | switch result { 58 | case let .failure(error): 59 | completion(.failure(error)) 60 | case let .success(database): 61 | do { 62 | try completion(.success(read.perform(in: database))) 63 | } catch { 64 | completion(.failure(error)) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | /// @mockable 72 | protocol PersistenceServiceProtocol { 73 | func performWrite(_ write: PersistenceServiceWrite, completion: @escaping (Error?) -> Void) 74 | func performWriteSync(_ write: PersistenceServiceWrite) throws 75 | 76 | func performRead(_ read: Read, completion: @escaping (Result) -> Void) where Read: PersistenceServiceRead 77 | func performReadSync(_ read: Read) throws -> Read.Model where Read: PersistenceServiceRead 78 | } 79 | 80 | extension PersistenceService: PersistenceServiceProtocol {} 81 | 82 | protocol HasPersistenceService { 83 | var persistenceService: PersistenceServiceProtocol { get } 84 | } 85 | -------------------------------------------------------------------------------- /App/Sources/Services/PreferencesService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class PreferencesService { 4 | private let notificationCenter: NotificationCenter 5 | private let userDefaults: UserDefaults 6 | 7 | init(notificationCenter: NotificationCenter = .default, userDefaults: UserDefaults = .standard) { 8 | self.notificationCenter = notificationCenter 9 | self.userDefaults = userDefaults 10 | } 11 | 12 | func set(_ value: Any?, forKey key: String) { 13 | userDefaults.set(value, forKey: key) 14 | 15 | let notification = Notification(name: .didChangeValue, userInfo: ["key": key]) 16 | notificationCenter.post(notification) 17 | } 18 | 19 | func value(forKey key: String) -> Any? { 20 | userDefaults.object(forKey: key) 21 | } 22 | 23 | func removeValue(forKey key: String) { 24 | userDefaults.removeObject(forKey: key) 25 | 26 | let notification = Notification(name: .didChangeValue, userInfo: ["key": key]) 27 | notificationCenter.post(notification) 28 | } 29 | 30 | func addObserver(forKey key: String, using handler: @escaping () -> Void) -> NSObjectProtocol { 31 | notificationCenter.addObserver(forName: .didChangeValue, object: nil, queue: .main) { notification in 32 | if let changedKey = notification.userInfo?["key"] as? String, changedKey == key { 33 | handler() 34 | } 35 | } 36 | } 37 | 38 | func removeObserver(_ observer: NSObjectProtocol) { 39 | notificationCenter.removeObserver(observer) 40 | } 41 | } 42 | 43 | private extension Notification.Name { 44 | static let didChangeValue = NSNotification.Name("com.mttcrsp.ansia.PreferencesService.didChangeValue") 45 | } 46 | 47 | /// @mockable 48 | protocol PreferencesServiceProtocol { 49 | func set(_ value: Any?, forKey key: String) 50 | func value(forKey key: String) -> Any? 51 | func removeValue(forKey key: String) 52 | 53 | func addObserver(forKey key: String, using handler: @escaping () -> Void) -> NSObjectProtocol 54 | func removeObserver(_ observer: NSObjectProtocol) 55 | } 56 | 57 | extension PreferencesService: PreferencesServiceProtocol {} 58 | -------------------------------------------------------------------------------- /App/Sources/Services/PreloadService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// @mockable 4 | protocol PreloadServiceFile { 5 | func fileExists(atPath path: String) -> Bool 6 | func copyItem(atPath srcPath: String, toPath dstPath: String) throws 7 | func url(for directory: FileManager.SearchPathDirectory, in domain: FileManager.SearchPathDomainMask, appropriateFor url: URL?, create shouldCreate: Bool) throws -> URL 8 | func removeItem(atPath path: String) throws 9 | } 10 | 11 | /// @mockable 12 | protocol PreloadServiceBundle { 13 | func path(forResource name: String?, ofType ext: String?) -> String? 14 | } 15 | 16 | final class PreloadService { 17 | enum Error: CustomNSError { 18 | case resourceNotFound 19 | } 20 | 21 | private let oldPath: String 22 | private let newPath: String 23 | private let bundle: PreloadServiceBundle 24 | private let fileManager: PreloadServiceFile 25 | 26 | init(bundle: PreloadServiceBundle = Bundle.main, fileManager: PreloadServiceFile = FileManager.default) throws { 27 | self.fileManager = fileManager 28 | self.bundle = bundle 29 | 30 | let fileName = "db", fileExtension = "sqlite" 31 | guard let oldPath = bundle.path(forResource: fileName, ofType: fileExtension) else { 32 | throw Error.resourceNotFound 33 | } 34 | 35 | let applicationSupportURL = try fileManager.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) 36 | let applicationDatabaseURL = applicationSupportURL.appendingPathComponent(fileName).appendingPathExtension(fileExtension) 37 | let newPath = applicationDatabaseURL.path 38 | 39 | self.oldPath = oldPath 40 | self.newPath = newPath 41 | } 42 | 43 | var databasePath: String { 44 | newPath 45 | } 46 | 47 | func removeDatabase() throws { 48 | try fileManager.removeItem(atPath: newPath) 49 | } 50 | 51 | func preloadDatabaseIfNeeded() throws { 52 | if !fileManager.fileExists(atPath: newPath) { 53 | try fileManager.copyItem(atPath: oldPath, toPath: newPath) 54 | } 55 | } 56 | } 57 | 58 | extension Bundle: PreloadServiceBundle {} 59 | 60 | extension FileManager: PreloadServiceFile {} 61 | -------------------------------------------------------------------------------- /App/Sources/Services/SoonService.swift: -------------------------------------------------------------------------------- 1 | final class SoonService { 2 | private let persistenceService: PersistenceServiceProtocol 3 | private let timeService: TimeServiceProtocol 4 | 5 | init(timeService: TimeServiceProtocol, persistenceService: PersistenceServiceProtocol) { 6 | self.persistenceService = persistenceService 7 | self.timeService = timeService 8 | } 9 | 10 | func loadEvents(completion: @escaping (Result<[Event], Error>) -> Void) { 11 | let operation = GetEventsStartingIn30Minutes(now: timeService.now) 12 | persistenceService.performRead(operation, completion: completion) 13 | } 14 | } 15 | 16 | // @mockable 17 | protocol SoonServiceProtocol { 18 | func loadEvents(completion: @escaping (Result<[Event], Error>) -> Void) 19 | } 20 | 21 | extension SoonService: SoonServiceProtocol {} 22 | 23 | protocol HasSoonService { 24 | var soonService: SoonServiceProtocol { get } 25 | } 26 | -------------------------------------------------------------------------------- /App/Sources/Services/UpdateService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class UpdateService { 4 | private let bundle: UpdateServiceBundle 5 | private let networkService: UpdateServiceNetwork 6 | 7 | init(networkService: UpdateServiceNetwork, bundle: UpdateServiceBundle = Bundle.main) { 8 | self.networkService = networkService 9 | self.bundle = bundle 10 | } 11 | 12 | func detectUpdates(completion: @escaping () -> Void) { 13 | guard let bundleIdentifier = bundle.bundleIdentifier else { 14 | return assertionFailure("Failed to acquire bundle identifier from bundle \(bundle)") 15 | } 16 | 17 | guard let bundleShortVersion = bundle.bundleShortVersion else { 18 | return assertionFailure("Failed to acquire short bundle version from bundle \(bundle)") 19 | } 20 | 21 | let request = AppStoreSearchRequest() 22 | networkService.perform(request) { result in 23 | guard case let .success(response) = result else { return } 24 | 25 | guard let result = response.results.first(where: { result in result.bundleIdentifier == bundleIdentifier }) else { 26 | return assertionFailure("AppStore search request did not return any application with identifier \(bundleIdentifier)") 27 | } 28 | 29 | if result.version.compare(bundleShortVersion, options: .numeric) == .orderedDescending { 30 | completion() 31 | } 32 | } 33 | } 34 | } 35 | 36 | /// @mockable 37 | protocol UpdateServiceProtocol: AnyObject { 38 | func detectUpdates(completion: @escaping () -> Void) 39 | } 40 | 41 | extension UpdateService: UpdateServiceProtocol {} 42 | 43 | /// @mockable 44 | protocol UpdateServiceBundle { 45 | var bundleIdentifier: String? { get } 46 | var bundleShortVersion: String? { get } 47 | } 48 | 49 | extension Bundle: UpdateServiceBundle {} 50 | 51 | /// @mockable 52 | protocol UpdateServiceNetwork { 53 | @discardableResult 54 | func perform(_ request: AppStoreSearchRequest, completion: @escaping (Result) -> Void) -> NetworkServiceTask 55 | } 56 | 57 | extension NetworkService: UpdateServiceNetwork {} 58 | 59 | protocol HasUpdateService { 60 | var updateService: UpdateServiceProtocol { get } 61 | } 62 | -------------------------------------------------------------------------------- /App/Sources/Services/VideosService.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | final class VideosService { 4 | struct Videos: Equatable { 5 | let watching, watched: [Event] 6 | } 7 | 8 | private let playbackService: PlaybackServiceProtocol 9 | private let persistenceService: PersistenceServiceProtocol 10 | 11 | init(playbackService: PlaybackServiceProtocol, persistenceService: PersistenceServiceProtocol) { 12 | self.playbackService = playbackService 13 | self.persistenceService = persistenceService 14 | } 15 | 16 | func loadVideos(_ completion: @escaping (Result) -> Void) { 17 | let group = DispatchGroup() 18 | var groupError: Error? 19 | var watching: [Event]? 20 | var watched: [Event]? 21 | 22 | group.enter() 23 | let watchedIdentifiers = playbackService.watched 24 | let watchedOperation = GetEventsByIdentifiers(identifiers: watchedIdentifiers) 25 | persistenceService.performRead(watchedOperation) { result in 26 | switch result { 27 | case let .failure(error): 28 | groupError = groupError ?? error 29 | case let .success(events): 30 | watched = events 31 | } 32 | group.leave() 33 | } 34 | 35 | group.enter() 36 | let watchingIdentifiers = playbackService.watching 37 | let watchingOperation = GetEventsByIdentifiers(identifiers: watchingIdentifiers) 38 | persistenceService.performRead(watchingOperation) { result in 39 | switch result { 40 | case let .failure(error): 41 | groupError = groupError ?? error 42 | case let .success(events): 43 | watching = events 44 | } 45 | group.leave() 46 | } 47 | 48 | group.notify(queue: .main) { 49 | if let error = groupError { 50 | completion(.failure(error)) 51 | } else if let watching, let watched { 52 | completion(.success(Videos(watching: watching, watched: watched))) 53 | } 54 | } 55 | } 56 | } 57 | 58 | /// @mockable 59 | protocol VideosServiceProtocol { 60 | func loadVideos(_ completion: @escaping (Result) -> Void) 61 | } 62 | 63 | extension VideosService: VideosServiceProtocol {} 64 | 65 | protocol HasVideosService { 66 | var videosService: VideosServiceProtocol { get } 67 | } 68 | -------------------------------------------------------------------------------- /App/Sources/ViewControllers/AcknowledgementsViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// @mockable 4 | protocol AcknowledgementsViewControllerDataSource: AnyObject { 5 | var acknowledgements: [Acknowledgement] { get } 6 | } 7 | 8 | /// @mockable 9 | protocol AcknowledgementsViewControllerDelegate: AnyObject { 10 | func acknowledgementsViewController(_ acknowledgementsViewController: AcknowledgementsViewController, didSelect acknowledgement: Acknowledgement) 11 | } 12 | 13 | final class AcknowledgementsViewController: UITableViewController { 14 | weak var dataSource: AcknowledgementsViewControllerDataSource? 15 | weak var delegate: AcknowledgementsViewControllerDelegate? 16 | 17 | var acknowledgements: [Acknowledgement] { 18 | dataSource?.acknowledgements ?? [] 19 | } 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | tableView.estimatedRowHeight = 44 24 | tableView.rowHeight = UITableView.automaticDimension 25 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: UITableViewCell.reuseIdentifier) 26 | } 27 | 28 | override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { 29 | acknowledgements.count 30 | } 31 | 32 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 33 | let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCell.reuseIdentifier, for: indexPath) 34 | cell.configure(with: acknowledgement(at: indexPath)) 35 | return cell 36 | } 37 | 38 | override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { 39 | delegate?.acknowledgementsViewController(self, didSelect: acknowledgement(at: indexPath)) 40 | } 41 | 42 | private func acknowledgement(at indexPath: IndexPath) -> Acknowledgement { 43 | acknowledgements[indexPath.row] 44 | } 45 | } 46 | 47 | private extension UITableViewCell { 48 | func configure(with acknowledgement: Acknowledgement) { 49 | accessoryType = .disclosureIndicator 50 | textLabel?.text = acknowledgement.name 51 | textLabel?.font = .fos_preferredFont(forTextStyle: .body) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /App/Sources/ViewControllers/BlueprintsEmptyViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class BlueprintsEmptyViewController: UIViewController { 4 | private let label = UILabel() 5 | 6 | override func viewDidLoad() { 7 | super.viewDidLoad() 8 | 9 | view.addSubview(label) 10 | view.backgroundColor = .tertiarySystemBackground 11 | 12 | label.textAlignment = .center 13 | label.textColor = .secondaryLabel 14 | label.accessibilityIdentifier = "empty_blueprints" 15 | label.text = L10n.Map.Blueprint.empty 16 | label.font = .fos_preferredFont(forTextStyle: .body, withSymbolicTraits: .traitItalic) 17 | } 18 | 19 | override func viewDidLayoutSubviews() { 20 | super.viewDidLayoutSubviews() 21 | label.sizeToFit() 22 | label.center.x = view.layoutMarginsGuide.layoutFrame.midX 23 | label.center.y = view.layoutMarginsGuide.layoutFrame.midY 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /App/Sources/ViewControllers/DateViewController.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | 3 | import UIKit 4 | 5 | protocol DateViewControllerDelegate: AnyObject { 6 | func dateViewControllerDidChange(_ dateViewController: DateViewController) 7 | } 8 | 9 | final class DateViewController: UIViewController { 10 | weak var delegate: DateViewControllerDelegate? 11 | 12 | private lazy var datePicker = UIDatePicker() 13 | 14 | var date: Date { 15 | get { datePicker.date } 16 | set { datePicker.date = newValue } 17 | } 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | view.addSubview(datePicker) 23 | view.backgroundColor = .systemBackground 24 | 25 | datePicker.translatesAutoresizingMaskIntoConstraints = false 26 | datePicker.addTarget(self, action: #selector(didChangeDate), for: .valueChanged) 27 | 28 | NSLayoutConstraint.activate([ 29 | datePicker.centerYAnchor.constraint(equalTo: view.centerYAnchor), 30 | datePicker.leadingAnchor.constraint(equalTo: view.leadingAnchor), 31 | datePicker.trailingAnchor.constraint(equalTo: view.trailingAnchor), 32 | ]) 33 | } 34 | 35 | @objc private func didChangeDate() { 36 | delegate?.dateViewControllerDidChange(self) 37 | } 38 | } 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /App/Sources/ViewControllers/EmbeddedBlueprintViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class EmbeddedBlueprintViewController: UIViewController { 4 | var blueprint: Blueprint? { 5 | didSet { didChangeBlueprint() } 6 | } 7 | 8 | private lazy var imageView = UIImageView() 9 | 10 | override func viewDidLoad() { 11 | super.viewDidLoad() 12 | view.addSubview(imageView) 13 | 14 | imageView.contentMode = .scaleAspectFit 15 | imageView.translatesAutoresizingMaskIntoConstraints = false 16 | 17 | NSLayoutConstraint.activate([ 18 | imageView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 8), 19 | imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8), 20 | imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), 21 | imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), 22 | ]) 23 | } 24 | 25 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 26 | super.traitCollectionDidChange(previousTraitCollection) 27 | 28 | if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle { 29 | didChangeBlueprint() 30 | } 31 | } 32 | 33 | private func didChangeBlueprint() { 34 | guard let blueprint, let image = UIImage(named: blueprint.imageName) else { 35 | imageView.image = nil 36 | return 37 | } 38 | 39 | if traitCollection.userInterfaceStyle == .dark { 40 | imageView.image = image.inverted 41 | } else { 42 | imageView.image = image 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /App/Sources/ViewControllers/EventsSearchController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol EventsSearchController: UIViewController { 4 | var results: [Event] { get set } 5 | var resultsViewController: EventsViewController? { get } 6 | var persistenceService: PersistenceServiceProtocol { get } 7 | } 8 | 9 | extension EventsSearchController { 10 | func didChangeQuery(_ query: String) { 11 | guard query.count >= 3 else { 12 | resultsViewController?.configure(with: .noQuery) 13 | resultsViewController?.setEvents([]) 14 | return 15 | } 16 | 17 | let operation = GetEventsBySearch(query: query) 18 | persistenceService.performRead(operation) { [weak self] result in 19 | DispatchQueue.main.async { 20 | switch result { 21 | case .failure: 22 | self?.resultsViewController?.configure(with: .failure(query: query)) 23 | self?.resultsViewController?.setEvents([]) 24 | case let .success(events): 25 | self?.resultsViewController?.configure(with: .success(query: query)) 26 | self?.resultsViewController?.setEvents(events) 27 | } 28 | } 29 | } 30 | } 31 | } 32 | 33 | private extension EventsViewController { 34 | enum Configuration { 35 | case noQuery 36 | case success(query: String) 37 | case failure(query: String) 38 | } 39 | 40 | func configure(with configuration: Configuration) { 41 | view.isHidden = configuration.isViewHidden 42 | emptyBackgroundTitle = configuration.emptyBackgroundTitle 43 | emptyBackgroundMessage = configuration.emptyBackgroundMessage 44 | } 45 | } 46 | 47 | extension EventsViewController.Configuration { 48 | var emptyBackgroundTitle: String? { 49 | switch self { 50 | case .noQuery: 51 | nil 52 | case .failure: 53 | L10n.Search.Error.title 54 | case .success: 55 | L10n.Search.Empty.title 56 | } 57 | } 58 | 59 | var emptyBackgroundMessage: String? { 60 | switch self { 61 | case .noQuery: 62 | nil 63 | case .failure: 64 | L10n.Search.Error.message 65 | case let .success(query): 66 | L10n.Search.Empty.message(query) 67 | } 68 | } 69 | 70 | var isViewHidden: Bool { 71 | switch self { 72 | case .noQuery: 73 | true 74 | case .success, .failure: 75 | false 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /App/Sources/ViewControllers/FullscreenBlueprintViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class FullscreenBlueprintViewController: UIViewController { 4 | var blueprint: Blueprint? { 5 | didSet { didChangeBlueprint() } 6 | } 7 | 8 | private lazy var imageView = ScrollImageView() 9 | 10 | override func loadView() { 11 | view = imageView 12 | } 13 | 14 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 15 | super.traitCollectionDidChange(previousTraitCollection) 16 | 17 | if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle { 18 | didChangeBlueprint() 19 | } 20 | } 21 | 22 | private func didChangeBlueprint() { 23 | guard let blueprint, let image = UIImage(named: blueprint.imageName) else { 24 | imageView.image = nil 25 | return 26 | } 27 | 28 | if traitCollection.userInterfaceStyle == .dark { 29 | imageView.image = image.inverted 30 | } else { 31 | imageView.image = image 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /App/Sources/ViewControllers/MoreViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// @mockable 4 | protocol MoreViewControllerDelegate: AnyObject { 5 | func moreViewController(_ moreViewController: MoreViewController, didSelect item: MoreItem) 6 | } 7 | 8 | final class MoreViewController: UITableViewController { 9 | weak var delegate: MoreViewControllerDelegate? 10 | 11 | private let sections: [MoreSection] 12 | 13 | init(sections: [MoreSection], style: UITableView.Style) { 14 | self.sections = sections 15 | super.init(style: style) 16 | } 17 | 18 | required init?(coder: NSCoder) { 19 | fatalError("init(coder:) has not been implemented") 20 | } 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | tableView.estimatedRowHeight = 44 25 | tableView.rowHeight = UITableView.automaticDimension 26 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: UITableViewCell.reuseIdentifier) 27 | } 28 | 29 | override func numberOfSections(in _: UITableView) -> Int { 30 | sections.count 31 | } 32 | 33 | override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { 34 | sections[section].title 35 | } 36 | 37 | override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { 38 | sections[section].items.count 39 | } 40 | 41 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 42 | let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCell.reuseIdentifier, for: indexPath) 43 | cell.configure(with: item(at: indexPath)) 44 | return cell 45 | } 46 | 47 | override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { 48 | delegate?.moreViewController(self, didSelect: item(at: indexPath)) 49 | } 50 | 51 | private func item(at indexPath: IndexPath) -> MoreItem { 52 | sections[indexPath.section].items[indexPath.row] 53 | } 54 | } 55 | 56 | private extension UITableViewCell { 57 | func configure(with item: MoreItem) { 58 | accessibilityIdentifier = item.accessibilityIdentifier 59 | textLabel?.font = .fos_preferredFont(forTextStyle: .body) 60 | textLabel?.text = item.title 61 | textLabel?.numberOfLines = 0 62 | imageView?.image = item.icon 63 | accessoryType = .disclosureIndicator 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /App/Sources/ViewControllers/TextViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class TextViewController: UIViewController { 4 | var attributedText: NSAttributedString? { 5 | get { textView.attributedText } 6 | set { textView.attributedText = newValue } 7 | } 8 | 9 | var accessibilityIdentifier: String? { 10 | get { textView.accessibilityIdentifier } 11 | set { textView.accessibilityIdentifier = newValue } 12 | } 13 | 14 | private lazy var textView = UITextView() 15 | 16 | override func loadView() { 17 | view = textView 18 | } 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | textView.isEditable = false 23 | textView.backgroundColor = .systemGroupedBackground 24 | textView.adjustsFontForContentSizeCategory = true 25 | textView.textContainer.lineFragmentPadding = 0 26 | } 27 | 28 | override func viewDidLayoutSubviews() { 29 | super.viewDidLayoutSubviews() 30 | 31 | let verticalInset: CGFloat = 20 32 | let horizontalInset = max(10, textView.readableContentGuide.layoutFrame.minX) 33 | textView.textContainerInset.top = verticalInset 34 | textView.textContainerInset.bottom = verticalInset 35 | textView.textContainerInset.left = horizontalInset 36 | textView.textContainerInset.right = horizontalInset 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /App/Sources/Views/Action.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | struct Action { 4 | enum Style { 5 | case `default`, destructive 6 | } 7 | 8 | let title: String 9 | let image: UIImage? 10 | let style: Style 11 | let handler: () -> Void 12 | 13 | init(title: String, image: UIImage? = nil, style: Style = .default, handler: @escaping () -> Void = {}) { 14 | self.title = title 15 | self.image = image 16 | self.style = style 17 | self.handler = handler 18 | } 19 | } 20 | 21 | private extension UIContextualAction.Style { 22 | init(style: Action.Style) { 23 | switch style { 24 | case .default: 25 | self = .normal 26 | case .destructive: 27 | self = .destructive 28 | } 29 | } 30 | } 31 | 32 | @available(iOS 13.0, *) 33 | private extension UIMenuElement.Attributes { 34 | init(style: Action.Style) { 35 | switch style { 36 | case .default: 37 | self = [] 38 | case .destructive: 39 | self = [.destructive] 40 | } 41 | } 42 | } 43 | 44 | private extension UIContextualAction { 45 | convenience init(action: Action) { 46 | let style = UIContextualAction.Style(style: action.style) 47 | 48 | self.init(style: style, title: action.title) { _, _, completionHandler in 49 | action.handler() 50 | completionHandler(true) 51 | } 52 | 53 | switch action.style { 54 | case .default: 55 | backgroundColor = .systemBlue 56 | case .destructive: 57 | break 58 | } 59 | 60 | image = action.image 61 | } 62 | } 63 | 64 | @available(iOS 13.0, *) 65 | private extension UIAction { 66 | convenience init(action: Action) { 67 | let attributes = UIAction.Attributes(style: action.style) 68 | 69 | self.init(title: action.title, image: action.image, attributes: attributes) { _ in 70 | action.handler() 71 | } 72 | } 73 | } 74 | 75 | extension UISwipeActionsConfiguration { 76 | convenience init?(actions: [Action]) { 77 | guard !actions.isEmpty else { return nil } 78 | self.init(actions: actions.map(UIContextualAction.init)) 79 | } 80 | } 81 | 82 | @available(iOS 13.0, *) 83 | extension UIContextMenuConfiguration { 84 | convenience init?(actions: [Action]) { 85 | guard !actions.isEmpty else { return nil } 86 | 87 | let children = actions.map(UIAction.init) 88 | 89 | self.init( 90 | identifier: nil, 91 | previewProvider: nil, 92 | actionProvider: { _ in 93 | UIMenu(title: "", children: children) 94 | } 95 | ) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /App/Sources/Views/EventMetadataView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class EventMetadataView: UIView { 4 | var text: String? { 5 | get { label.text } 6 | set { label.text = newValue } 7 | } 8 | 9 | var image: UIImage? { 10 | get { imageView.image } 11 | set { imageView.image = newValue } 12 | } 13 | 14 | override var accessibilityLabel: String? { 15 | get { super.accessibilityLabel ?? text } 16 | set { super.accessibilityLabel = newValue } 17 | } 18 | 19 | private let label = UILabel() 20 | private let imageView = UIImageView() 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | 25 | isAccessibilityElement = true 26 | accessibilityTraits = .staticText 27 | 28 | label.numberOfLines = 0 29 | label.adjustsFontForContentSizeCategory = true 30 | label.font = .fos_preferredFont(forTextStyle: .subheadline) 31 | 32 | for subview in [imageView, label] { 33 | subview.translatesAutoresizingMaskIntoConstraints = false 34 | addSubview(subview) 35 | } 36 | 37 | NSLayoutConstraint.activate([ 38 | imageView.widthAnchor.constraint(equalToConstant: 24), 39 | imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor), 40 | 41 | imageView.leadingAnchor.constraint(equalTo: leadingAnchor), 42 | imageView.centerYAnchor.constraint(equalTo: label.centerYAnchor), 43 | imageView.trailingAnchor.constraint(equalTo: label.leadingAnchor, constant: -16), 44 | 45 | label.topAnchor.constraint(equalTo: topAnchor), 46 | label.bottomAnchor.constraint(equalTo: bottomAnchor), 47 | label.trailingAnchor.constraint(equalTo: trailingAnchor), 48 | ]) 49 | } 50 | 51 | @available(*, unavailable) 52 | required init?(coder _: NSCoder) { 53 | fatalError("init(coder:) has not been implemented") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /App/Sources/Views/LabelTableHeaderFooterView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class LabelTableHeaderFooterView: UITableViewHeaderFooterView { 4 | var text: String? { 5 | get { label.text } 6 | set { label.text = newValue } 7 | } 8 | 9 | var textColor: UIColor? { 10 | get { label.textColor } 11 | set { label.textColor = newValue } 12 | } 13 | 14 | var font: UIFont? { 15 | get { label.font } 16 | set { label.font = newValue } 17 | } 18 | 19 | private let label = UILabel() 20 | 21 | override init(reuseIdentifier: String?) { 22 | super.init(reuseIdentifier: reuseIdentifier) 23 | label.numberOfLines = 0 24 | label.textColor = .secondaryLabel 25 | label.adjustsFontForContentSizeCategory = true 26 | label.translatesAutoresizingMaskIntoConstraints = false 27 | label.font = .fos_preferredFont(forTextStyle: .callout) 28 | 29 | contentView.addSubview(label) 30 | 31 | NSLayoutConstraint.activate([ 32 | label.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), 33 | label.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor), 34 | label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), 35 | label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), 36 | ]) 37 | } 38 | 39 | @available(*, unavailable) 40 | required init?(coder _: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /App/Sources/Views/RoundedButton.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class RoundedButton: UIButton { 4 | private lazy var size = CGSize(width: 1, height: 1) 5 | private lazy var rect = CGRect(origin: .zero, size: size) 6 | private lazy var renderer = UIGraphicsImageRenderer(size: size) 7 | 8 | override init(frame: CGRect) { 9 | super.init(frame: frame) 10 | commonInit() 11 | } 12 | 13 | required init?(coder: NSCoder) { 14 | super.init(coder: coder) 15 | commonInit() 16 | } 17 | 18 | private func commonInit() { 19 | layer.cornerRadius = 4 20 | layer.masksToBounds = true 21 | 22 | imageView?.tintColor = .systemBackground 23 | setTitleColor(.systemBackground, for: .normal) 24 | setBackgroundImage(makeNormalImage(), for: .normal) 25 | setBackgroundImage(makeHighlightedImage(), for: .highlighted) 26 | 27 | contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12) 28 | titleLabel?.font = .fos_preferredFont(forTextStyle: .body, withSymbolicTraits: .traitBold) 29 | } 30 | 31 | override func tintColorDidChange() { 32 | super.tintColorDidChange() 33 | setBackgroundImage(makeNormalImage(), for: .normal) 34 | setBackgroundImage(makeHighlightedImage(), for: .highlighted) 35 | } 36 | 37 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 38 | super.traitCollectionDidChange(previousTraitCollection) 39 | 40 | if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle { 41 | setBackgroundImage(makeNormalImage(), for: .normal) 42 | setBackgroundImage(makeHighlightedImage(), for: .highlighted) 43 | } 44 | } 45 | 46 | private func makeNormalImage() -> UIImage? { 47 | renderer.image { context in 48 | context.cgContext.setFillColor(tintColor.cgColor) 49 | context.cgContext.fill(rect) 50 | } 51 | } 52 | 53 | private func makeHighlightedImage() -> UIImage? { 54 | renderer.image { context in 55 | context.cgContext.setFillColor(tintColor.withAlphaComponent(0.8).cgColor) 56 | context.cgContext.fill(rect) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /App/Sources/Views/TableBackgroundView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class TableBackgroundView: UIView { 4 | var title: String? { 5 | get { titleLabel.text } 6 | set { titleLabel.text = newValue } 7 | } 8 | 9 | var message: String? { 10 | get { messageLabel.text } 11 | set { messageLabel.text = newValue } 12 | } 13 | 14 | private let titleLabel = UILabel() 15 | private let messageLabel = UILabel() 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | 20 | for label in [titleLabel, messageLabel] { 21 | label.numberOfLines = 0 22 | label.textColor = .label 23 | label.textAlignment = .center 24 | label.adjustsFontForContentSizeCategory = true 25 | } 26 | 27 | titleLabel.accessibilityIdentifier = "background_title" 28 | titleLabel.font = .fos_preferredFont(forTextStyle: .title2, withSymbolicTraits: .traitBold) 29 | 30 | messageLabel.accessibilityIdentifier = "background_message" 31 | messageLabel.font = .fos_preferredFont(forTextStyle: .body, withSymbolicTraits: .traitItalic) 32 | 33 | let stackView = UIStackView(arrangedSubviews: [titleLabel, messageLabel]) 34 | stackView.translatesAutoresizingMaskIntoConstraints = false 35 | stackView.axis = .vertical 36 | stackView.spacing = 16 37 | addSubview(stackView) 38 | 39 | NSLayoutConstraint.activate([ 40 | stackView.centerYAnchor.constraint(equalTo: readableContentGuide.centerYAnchor), 41 | stackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), 42 | stackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), 43 | ]) 44 | } 45 | 46 | @available(*, unavailable) 47 | required init?(coder _: NSCoder) { 48 | fatalError("init(coder:) has not been implemented") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /App/Sources/Views/TrackView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class TrackView: UIView { 4 | var track: String? { 5 | didSet { didChangeTrack() } 6 | } 7 | 8 | private let label = UILabel() 9 | 10 | override init(frame: CGRect) { 11 | super.init(frame: frame) 12 | commonInit() 13 | } 14 | 15 | required init?(coder: NSCoder) { 16 | super.init(coder: coder) 17 | commonInit() 18 | } 19 | 20 | private func commonInit() { 21 | addSubview(label) 22 | 23 | isAccessibilityElement = true 24 | accessibilityTraits = .staticText 25 | 26 | layer.borderWidth = 1 27 | layer.borderColor = UIColor.label.cgColor 28 | layer.cornerRadius = 4 29 | 30 | label.font = .fos_preferredFont(forTextStyle: .callout) 31 | label.translatesAutoresizingMaskIntoConstraints = false 32 | label.adjustsFontForContentSizeCategory = true 33 | 34 | NSLayoutConstraint.activate([ 35 | label.topAnchor.constraint(equalTo: topAnchor, constant: 8), 36 | label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8), 37 | label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), 38 | label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), 39 | ]) 40 | } 41 | 42 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 43 | super.traitCollectionDidChange(previousTraitCollection) 44 | 45 | if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle { 46 | layer.borderColor = UIColor.label.cgColor 47 | } 48 | } 49 | 50 | private func didChangeTrack() { 51 | accessibilityLabel = track 52 | label.text = track?.uppercased() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /App/Sources/main.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | var app: UIApplication.Type = App.self 4 | var appDelegate: UIApplicationDelegate.Type = AppDelegate.self 5 | 6 | #if DEBUG 7 | private final class TestApp: UIApplication {} 8 | private final class TestAppDelegate: NSObject, UIApplicationDelegate {} 9 | 10 | if ProcessInfo.processInfo.environment["XCTestBundlePath"] != nil { 11 | app = TestApp.self 12 | appDelegate = TestAppDelegate.self 13 | } 14 | #endif 15 | 16 | UIApplicationMain( 17 | CommandLine.argc, 18 | CommandLine.unsafeArgv, 19 | NSStringFromClass(app), 20 | NSStringFromClass(appDelegate) 21 | ) 22 | -------------------------------------------------------------------------------- /App/Tests/AppStoreSearchRequestTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class AppStoreSearchRequestTests: XCTestCase { 6 | func testDecode() throws { 7 | let result1 = AppStoreSearchResult(bundleIdentifier: "nl.netsense.FOSDEM", version: "1.2.2") 8 | let result2 = AppStoreSearchResult(bundleIdentifier: "com.zerokspot.fosdem-to-go", version: "0.1") 9 | let response = AppStoreSearchResponse(results: [result1, result2]) 10 | let requestURL = URL(string: "https://itunes.apple.com/us/search?term=fosdem&media=software&entity=software")! 11 | let request = AppStoreSearchRequest() 12 | XCTAssertEqual(request.url, requestURL) 13 | 14 | let data = try BundleDataLoader().data(forResource: "results", withExtension: "json") 15 | XCTAssertEqual(try request.decode(data, response: nil), response) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /App/Tests/BuildingTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class BuildingTests: XCTestCase { 6 | func testDecode() throws { 7 | let resources = ["j", "aw", "f", "h", "j", "k", "u"] 8 | 9 | for resource in resources { 10 | let data = try BundleDataLoader().data(forResource: resource, withExtension: "json") 11 | let building = try JSONDecoder().decode(Building.self, from: data) 12 | XCTAssertNotNil(building.title, "Invalid title for building '\(resource)'") 13 | XCTAssertFalse(building.glyph.isEmpty, "Invalid glyph for building '\(resource)'") 14 | XCTAssertGreaterThanOrEqual(building.polygon.pointCount, 4, "Invalid polygon for building '\(resource)'") 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /App/Tests/BuildingsServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class BuildingsServiceTests: XCTestCase { 6 | func testLoadBuildings() throws { 7 | let building = "aw" 8 | let data = try BundleDataLoader().data(forResource: building, withExtension: "json") 9 | 10 | let bundle = BuildingsServiceBundleMock() 11 | bundle.dataHandler = { _, _ in data } 12 | 13 | let service = BuildingsService(bundleService: bundle, queue: .main) 14 | let expectation = expectation(description: #function) 15 | 16 | service.loadBuildings { buildings, error in 17 | XCTAssertEqual(buildings.count, 7) 18 | XCTAssertNil(error) 19 | expectation.fulfill() 20 | } 21 | 22 | waitForExpectations(timeout: 1) 23 | } 24 | 25 | func testLoadBuildingsMissingData() { 26 | let error = NSError(domain: "test", code: 1) 27 | let bundle = BuildingsServiceBundleMock() 28 | bundle.dataHandler = { _, _ in throw error } 29 | 30 | let service = BuildingsService(bundleService: bundle, queue: .main) 31 | let expectation = expectation(description: #function) 32 | 33 | service.loadBuildings { buildings, error in 34 | let error1 = error as NSError? 35 | let error2 = BuildingsService.Error.missingData as NSError 36 | 37 | XCTAssertTrue(buildings.isEmpty) 38 | XCTAssertEqual(error1, error2) 39 | expectation.fulfill() 40 | } 41 | 42 | waitForExpectations(timeout: 1) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /App/Tests/BundleDataLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | final class BundleDataLoader { 5 | private let bundle: Bundle 6 | 7 | init(bundle: Bundle = Bundle(for: BundleDataLoader.self)) { 8 | self.bundle = bundle 9 | } 10 | 11 | func data(forResource resource: String, withExtension ext: String) throws -> Data { 12 | let url = try XCTUnwrap(bundle.url(forResource: resource, withExtension: ext)) 13 | return try Data(contentsOf: url) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /App/Tests/BundleServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class BundleServiceTests: XCTestCase { 6 | func testData() throws { 7 | let bundleURL = URL(fileURLWithPath: "/fosdem") 8 | let bundle = BundleServiceBundleMock() 9 | bundle.urlHandler = { _, _ in bundleURL } 10 | 11 | let dataProviderData = Data("something".utf8) 12 | let dataProvider = BundleServiceDataProviderMock() 13 | dataProvider.dataHandler = { _ in dataProviderData } 14 | 15 | let resource = "resource", ext = "json" 16 | let service = BundleService(bundle: bundle, dataProvider: dataProvider) 17 | let data = try service.data(forResource: resource, withExtension: ext) 18 | 19 | XCTAssertEqual(data, dataProviderData) 20 | XCTAssertEqual(bundle.urlCallCount, 1) 21 | XCTAssertEqual(bundle.urlArgValues.first?.1, ext) 22 | XCTAssertEqual(bundle.urlArgValues.first?.0, resource) 23 | XCTAssertEqual(dataProvider.dataCallCount, 1) 24 | XCTAssertEqual(dataProvider.dataArgValues.first, bundleURL) 25 | } 26 | 27 | func testErrorData() { 28 | do { 29 | let bundle = BundleServiceBundleMock() 30 | bundle.urlHandler = { _, _ in nil } 31 | 32 | let dataProvider = BundleServiceDataProviderMock() 33 | dataProvider.dataHandler = { _ in Data() } 34 | 35 | let service = BundleService(bundle: bundle, dataProvider: dataProvider) 36 | _ = try service.data(forResource: "resource", withExtension: "ext") 37 | XCTFail("Unexpectedly succeeded in loading from bundle service") 38 | } catch { 39 | let error1 = error as NSError 40 | let error2 = BundleService.Error.resourceNotFound as NSError 41 | XCTAssertEqual(error1, error2) 42 | } 43 | } 44 | 45 | func testErrorBundle() { 46 | let error1 = NSError(domain: "test", code: 1) 47 | 48 | do { 49 | let bundle = BundleServiceBundleMock() 50 | bundle.urlHandler = { _, _ in URL(fileURLWithPath: "/fosdem") } 51 | 52 | let dataProvider = BundleServiceDataProviderMock() 53 | dataProvider.dataHandler = { _ in throw error1 } 54 | 55 | let service = BundleService(bundle: bundle, dataProvider: dataProvider) 56 | _ = try service.data(forResource: "resource", withExtension: "ext") 57 | XCTFail("Unexpectedly succeeded in loading from bundle service") 58 | } catch let error2 { 59 | let error1 = error1 as NSError 60 | let error2 = error2 as NSError 61 | XCTAssertEqual(error1, error2) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /App/Tests/DateComponentsFormatterTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class DateComponentsFormatterTests: XCTestCase { 6 | func testTime() { 7 | let formatter = DateComponentsFormatter.time 8 | XCTAssertEqual(formatter.string(from: .init(hour: 9, minute: 0)), "9:00") 9 | XCTAssertEqual(formatter.string(from: .init(hour: 23, minute: 59)), "23:59") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Tests/Event+Extensions.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import Foundation 4 | 5 | extension Event { 6 | static func from(_ string: String) throws -> Event { 7 | try JSONDecoder().decode(Event.self, from: Data(string.utf8)) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /App/Tests/InfoServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class InfoServiceTests: XCTestCase { 6 | func testLoadAttributedText() { 7 | let htmlString = #"

standard bold link

"# 8 | let htmlData = Data(htmlString.utf8) 9 | 10 | let bundle = InfoServiceBundleMock() 11 | bundle.dataHandler = { _, _ in htmlData } 12 | 13 | let expectation = expectation(description: #function) 14 | 15 | let service = InfoService(queue: .main, bundleService: bundle) 16 | service.loadAttributedText(for: .bus) { result in 17 | guard case let .success(attributedText) = result else { 18 | return XCTFail("Unexpectedly returned nil while loading attributed text") 19 | } 20 | 21 | let string = attributedText.string 22 | let lowerbound = string.startIndex 23 | let upperbound = string.endIndex 24 | let range = NSRange(lowerbound ..< upperbound, in: string) 25 | 26 | var attributesData: [([NSAttributedString.Key: Any], NSRange)] = [] 27 | attributedText.enumerateAttributes(in: range) { attributes, range, _ in 28 | attributesData.append((attributes, range)) 29 | } 30 | 31 | XCTAssertEqual(bundle.dataCallCount, 1) 32 | XCTAssertEqual(bundle.dataArgValues.first?.0, "bus-tram") 33 | XCTAssertEqual(bundle.dataArgValues.first?.1, "html") 34 | XCTAssertEqual(attributesData.count, 5) 35 | 36 | expectation.fulfill() 37 | } 38 | 39 | waitForExpectations(timeout: 1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /App/Tests/LaunchServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class LaunchServiceTests: XCTestCase { 6 | func testDetect() throws { 7 | let defaults = LaunchServiceDefaultsMock() 8 | let bundle = LaunchServiceBundleMock() 9 | bundle.bundleShortVersion = "1.0.0" 10 | 11 | let service = LaunchService(fosdemYear: 2021, bundle: bundle, defaults: defaults) 12 | try service.detectStatus() 13 | XCTAssertFalse(service.didLaunchAfterUpdate) 14 | XCTAssertTrue(service.didLaunchAfterInstall) 15 | 16 | try service.detectStatus() 17 | XCTAssertFalse(service.didLaunchAfterUpdate) 18 | XCTAssertFalse(service.didLaunchAfterInstall) 19 | 20 | bundle.bundleShortVersion = "1.0.1" 21 | try service.detectStatus() 22 | XCTAssertTrue(service.didLaunchAfterUpdate) 23 | XCTAssertFalse(service.didLaunchAfterInstall) 24 | 25 | try service.detectStatus() 26 | XCTAssertFalse(service.didLaunchAfterUpdate) 27 | XCTAssertFalse(service.didLaunchAfterInstall) 28 | } 29 | 30 | func testDetectMissingBundleShortVersion() { 31 | let bundle = LaunchServiceBundleMock() 32 | let defaults = LaunchServiceDefaultsMock() 33 | let service = LaunchService(fosdemYear: 2021, bundle: bundle, defaults: defaults) 34 | do { 35 | try service.detectStatus() 36 | } catch { 37 | let error1 = error as NSError 38 | let error2 = LaunchService.Error.versionDetectionFailed as NSError 39 | XCTAssertEqual(error1, error2) 40 | } 41 | } 42 | } 43 | 44 | final class LaunchServiceBundleMock: LaunchServiceBundle { 45 | var bundleShortVersion: String? 46 | } 47 | 48 | final class LaunchServiceDefaultsMock: LaunchServiceDefaults { 49 | private var dictionary: [String: String] = [:] 50 | 51 | func string(forKey key: String) -> String? { 52 | dictionary[key] 53 | } 54 | 55 | func set(_ value: Any?, forKey defaultName: String) { 56 | dictionary[defaultName] = value as? String 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /App/Tests/LinkTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class LinkTests: XCTestCase { 6 | func testLivestreamURL1() { 7 | let link = Link(name: "test", url: URL(string: "https://live.fosdem.org/watch/mcommunity")) 8 | XCTAssertEqual(link.livestreamURL, URL(string: "https://stream.fosdem.org/mcommunity.m3u8")) 9 | } 10 | 11 | func testLivestreamURL2() { 12 | let link = Link(name: "test", url: URL(string: "https://live.fosdem.org/watch/dwebperformance")) 13 | XCTAssertEqual(link.livestreamURL, URL(string: "https://stream.fosdem.org/dwebperformance.m3u8")) 14 | } 15 | 16 | func testIsLivestream() { 17 | let link = Link(name: "test", url: URL(string: "https://live.fosdem.org/watch/dwebperformance")) 18 | XCTAssertTrue(link.isLivestream) 19 | } 20 | 21 | func testIsMP4Video() { 22 | let link = Link(name: "test", url: URL(string: "https://video.fosdem.org/2023/Janson/celebrating_25_years_of_open_source.mp4")) 23 | XCTAssertTrue(link.isMP4Video) 24 | } 25 | 26 | func testIsWEBMVideo() { 27 | let link = Link(name: "test", url: URL(string: "https://video.fosdem.org/2023/Janson/celebrating_25_years_of_open_source.webm")) 28 | XCTAssertTrue(link.isWEBMVideo) 29 | } 30 | 31 | func testIsVideoMP4() { 32 | let link = Link(name: "test", url: URL(string: "https://video.fosdem.org/2023/Janson/celebrating_25_years_of_open_source.webm")) 33 | XCTAssertTrue(link.isVideo) 34 | } 35 | 36 | func testIsVideoWEBM() { 37 | let link = Link(name: "test", url: URL(string: "https://video.fosdem.org/2023/Janson/celebrating_25_years_of_open_source.mp4")) 38 | XCTAssertTrue(link.isVideo) 39 | } 40 | 41 | func testIsAddition() { 42 | let link = Link(name: "test", url: URL(string: "https://anniv.co")) 43 | XCTAssertTrue(link.isAddition) 44 | } 45 | 46 | func testIsAdditionIgnoresFeedback() { 47 | let link = Link(name: "test", url: URL(string: "https://submission.fosdem.org/feedback/14956.php")) 48 | XCTAssertFalse(link.isAddition) 49 | } 50 | 51 | func testIsAdditionIgnoresChatWeb() throws { 52 | let url = try XCTUnwrap(URLComponents(string: "https://chat.fosdem.org/#/room/#2023-janson:fosdem.org")?.url) 53 | let link = Link(name: "test", url: url) 54 | XCTAssertFalse(link.isAddition) 55 | } 56 | 57 | func testIsAdditionIgnoresChatMatrix() throws { 58 | let url = try XCTUnwrap(URLComponents(string: "https://matrix.to/#/#2023-janson:fosdem.org?web-instance[element.io]=chat.fosdem.org")?.url) 59 | let link = Link(name: "test", url: url) 60 | XCTAssertFalse(link.isAddition) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /App/Tests/OpenServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class OpenServiceTests: XCTestCase { 6 | func testOpen() { 7 | let application = OpenServiceApplicationMock() 8 | application.openHandler = { _, _, completion in 9 | completion?(true) 10 | } 11 | 12 | let url = URL(fileURLWithPath: "test") 13 | let expectation = expectation(description: #function) 14 | let openService = OpenService(application: application) 15 | openService.open(url) { succeeded in 16 | XCTAssertTrue(succeeded) 17 | expectation.fulfill() 18 | } 19 | 20 | waitForExpectations(timeout: 1) 21 | XCTAssertEqual(application.openCallCount, 1) 22 | XCTAssertEqual(application.openArgValues.map(\.0).first, url) 23 | XCTAssertEqual(application.openArgValues.map(\.1).first?.isEmpty, true) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /App/Tests/PreferencesServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class PreferencesServiceTests: XCTestCase { 6 | func testEditing() throws { 7 | let userDefaultsDomain = "com.mttcrsp.test" 8 | let userDefaults = try XCTUnwrap(UserDefaults(suiteName: userDefaultsDomain)) 9 | defer { userDefaults.removeSuite(named: userDefaultsDomain) } 10 | 11 | let notificationCenter = NotificationCenter() 12 | let preferencesService = PreferencesService(notificationCenter: notificationCenter, userDefaults: userDefaults) 13 | preferencesService.set("value", forKey: "key") 14 | XCTAssertEqual(preferencesService.value(forKey: "key") as? String, "value") 15 | 16 | preferencesService.removeValue(forKey: "key") 17 | XCTAssertNil(preferencesService.value(forKey: "key")) 18 | } 19 | 20 | func testObserver() throws { 21 | let userDefaultsDomain = "com.mttcrsp.test" 22 | let userDefaults = try XCTUnwrap(UserDefaults(suiteName: userDefaultsDomain)) 23 | defer { userDefaults.removeSuite(named: userDefaultsDomain) } 24 | 25 | let notificationCenter = NotificationCenter() 26 | let preferencesService = PreferencesService(notificationCenter: notificationCenter, userDefaults: userDefaults) 27 | 28 | var callCount = 0 29 | let observer = preferencesService.addObserver(forKey: "key") { 30 | callCount += 1 31 | } 32 | 33 | preferencesService.set("value", forKey: "key") 34 | XCTAssertEqual(callCount, 1) 35 | preferencesService.set("value", forKey: "other") 36 | XCTAssertEqual(callCount, 1) 37 | preferencesService.removeValue(forKey: "key") 38 | XCTAssertEqual(callCount, 2) 39 | 40 | preferencesService.removeObserver(observer) 41 | preferencesService.set("value", forKey: "key") 42 | preferencesService.removeValue(forKey: "key") 43 | XCTAssertEqual(callCount, 2) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /App/Tests/ScheduleRequestTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class ScheduleRequestTests: XCTestCase { 6 | func testDecode() throws { 7 | let year = 2020 8 | let data = try XCTUnwrap(BundleDataLoader().data(forResource: "\(year)", withExtension: "xml")) 9 | 10 | let requestURL = URL(string: "https://fosdem.org/2020/schedule/xml") 11 | let request = ScheduleRequest(year: year) 12 | XCTAssertEqual(request.url, requestURL) 13 | XCTAssertNoThrow(try request.decode(data, response: nil)) 14 | } 15 | 16 | func testNotFound() { 17 | let request = ScheduleRequest(year: 2020) 18 | let response = HTTPURLResponse(url: request.url, statusCode: 404, httpVersion: nil, headerFields: nil) 19 | XCTAssertThrowsError(try request.decode(Data(), response: response)) { error in 20 | XCTAssertEqual(error as? ScheduleRequest.Error, .notFound) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App/Tests/ScheduleXMLParserTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class ScheduleXMLParserTests: XCTestCase { 6 | func testDecoding() throws { 7 | for year in 2007 ... 2025 { 8 | let data = try data(forYear: year) 9 | let parser = ScheduleXMLParser(data: data) 10 | XCTAssert(parser.parse()) 11 | XCTAssertNil(parser.parseError) 12 | XCTAssertNil(parser.validationError) 13 | 14 | let schedule = try XCTUnwrap(parser.schedule) 15 | XCTAssertGreaterThan(schedule.days.last?.events.count ?? 0, 0) 16 | XCTAssertGreaterThan(schedule.days.first?.events.count ?? 0, 0) 17 | } 18 | } 19 | 20 | func testLinks() throws { 21 | let data = try data(forYear: 2023) 22 | let parser = ScheduleXMLParser(data: data) 23 | XCTAssert(parser.parse()) 24 | 25 | let schedule = try XCTUnwrap(parser.schedule) 26 | let validLinks = schedule.days 27 | .flatMap(\.events) 28 | .flatMap(\.links) 29 | .filter { link in link.url != nil } 30 | XCTAssertEqual(validLinks.count, 4962) 31 | } 32 | 33 | private func data(forYear year: Int) throws -> Data { 34 | try XCTUnwrap(BundleDataLoader().data(forResource: "\(year)", withExtension: "xml")) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /App/Tests/TimeServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class TimeServiceTests: XCTestCase { 6 | func testAddObserver() { 7 | var block: ((TimeServiceTimer) -> Void)? 8 | 9 | let timer = TimeServiceTimerMock() 10 | timer.invalidateHandler = { 11 | block = nil 12 | } 13 | 14 | let timerProvider = TimeServiceProviderMock() 15 | timerProvider.scheduledTimerHandler = { _, _, receivedBlock in 16 | block = receivedBlock 17 | return timer 18 | } 19 | 20 | let timeService = TimeService(timerProvider: timerProvider) 21 | timeService.startMonitoring() 22 | 23 | var invocationsCount = 0 24 | _ = timeService.addObserver { 25 | invocationsCount += 1 26 | } 27 | 28 | block?(timer) 29 | block?(timer) 30 | block?(timer) 31 | 32 | XCTAssertEqual(invocationsCount, 3) 33 | } 34 | 35 | func testMonitoring() { 36 | var block: ((TimeServiceTimer) -> Void)? 37 | 38 | let timer = TimeServiceTimerMock() 39 | timer.invalidateHandler = { 40 | block = nil 41 | } 42 | 43 | let timerProvider = TimeServiceProviderMock() 44 | timerProvider.scheduledTimerHandler = { _, _, receivedBlock in 45 | block = receivedBlock 46 | return timer 47 | } 48 | 49 | let timeService = TimeService(timerProvider: timerProvider) 50 | 51 | var invocationsCount = 0 52 | _ = timeService.addObserver { 53 | invocationsCount += 1 54 | } 55 | 56 | timeService.startMonitoring() 57 | block?(timer) 58 | XCTAssertEqual(invocationsCount, 1) 59 | 60 | timeService.stopMonitoring() 61 | block?(timer) 62 | XCTAssertEqual(invocationsCount, 1) 63 | 64 | timeService.startMonitoring() 65 | block?(timer) 66 | XCTAssertEqual(invocationsCount, 2) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /App/Tests/TracksServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class TracksServiceTests: XCTestCase { 6 | func testLoadConfiguration() { 7 | let expectation = expectation(description: #function) 8 | 9 | let favoritesService = FavoritesServiceProtocolMock() 10 | favoritesService.containsTrackHandler = { track in 11 | ["2", "3"].contains(track.name) 12 | } 13 | 14 | let track1 = Track(name: "1", day: 1, date: .init()) 15 | let track2 = Track(name: "2", day: 1, date: .init()) 16 | let track3 = Track(name: "3", day: 2, date: .init()) 17 | let track4 = Track(name: "4", day: 2, date: .init()) 18 | let track5 = Track(name: "41", day: 2, date: .init()) 19 | let tracks = [track1, track2, track3, track4, track5] 20 | 21 | var read: Any? 22 | let persistenceService = PersistenceServiceProtocolMock() 23 | persistenceService.performReadHandler = { receivedRead, completion in 24 | read = receivedRead 25 | if let completion = completion as? ((Result<[Track], Error>) -> Void) { 26 | completion(.success(tracks)) 27 | } 28 | } 29 | 30 | let tracksService = TracksService(favoritesService: favoritesService, persistenceService: persistenceService) 31 | 32 | var configuration: TracksConfiguration? 33 | tracksService.loadConfiguration { value in 34 | configuration = value 35 | expectation.fulfill() 36 | } 37 | 38 | waitForExpectations(timeout: 1) 39 | 40 | XCTAssertEqual(read as? GetAllTracks, .init()) 41 | XCTAssertEqual(configuration?.filters, [.all, .day(1), .day(2)]) 42 | 43 | XCTAssertEqual(configuration?.filteredTracks.count, 3) 44 | XCTAssertEqual(configuration?.filteredTracks[.all], tracks) 45 | XCTAssertEqual(configuration?.filteredTracks[.day(1)], [track1, track2]) 46 | XCTAssertEqual(configuration?.filteredTracks[.day(2)], [track3, track4, track5]) 47 | 48 | XCTAssertEqual(configuration?.filteredFavoriteTracks.count, 3) 49 | XCTAssertEqual(configuration?.filteredFavoriteTracks[.all], [track2, track3]) 50 | XCTAssertEqual(configuration?.filteredFavoriteTracks[.day(1)], [track2]) 51 | XCTAssertEqual(configuration?.filteredFavoriteTracks[.day(2)], [track3]) 52 | 53 | XCTAssertEqual(configuration?.filteredIndexTitles.count, 3) 54 | XCTAssertEqual(configuration?.filteredIndexTitles[.all], ["1": 0, "2": 1, "3": 2, "4": 3]) 55 | XCTAssertEqual(configuration?.filteredIndexTitles[.day(1)], ["1": 0, "2": 1]) 56 | XCTAssertEqual(configuration?.filteredIndexTitles[.day(2)], ["3": 0, "4": 1]) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /App/Tests/UpdateServiceTests.swift: -------------------------------------------------------------------------------- 1 | @testable 2 | import Fosdem 3 | import XCTest 4 | 5 | final class UpdateServiceTests: XCTestCase { 6 | func testDetectUpdates() { 7 | let bundleIdentifier = "com.mttcrsp.fosdem" 8 | let bundle = UpdateServiceBundleMock(bundleIdentifier: bundleIdentifier, bundleShortVersion: "1.0.0") 9 | 10 | let result1 = AppStoreSearchResult(bundleIdentifier: "invalid identifier", version: "invalid version") 11 | let result2 = AppStoreSearchResult(bundleIdentifier: bundleIdentifier, version: "1.1.1") 12 | let response = AppStoreSearchResponse(results: [result1, result2]) 13 | let networkService = UpdateServiceNetworkMock() 14 | networkService.performHandler = { _, completion in 15 | completion(.success(response)) 16 | return NetworkServiceTaskMock() 17 | } 18 | 19 | var didDetectUpdates = false 20 | let service = UpdateService(networkService: networkService, bundle: bundle) 21 | service.detectUpdates { didDetectUpdates = true } 22 | XCTAssertTrue(didDetectUpdates) 23 | } 24 | 25 | func testDetectUpdatesNoUpdate() { 26 | let bundleIdentifier = "com.mttcrsp.fosdem" 27 | let bundle = UpdateServiceBundleMock(bundleIdentifier: bundleIdentifier, bundleShortVersion: "1.0.0") 28 | 29 | let result1 = AppStoreSearchResult(bundleIdentifier: bundleIdentifier, version: "1.0.0") 30 | let result2 = AppStoreSearchResult(bundleIdentifier: "invalid identifier", version: "2.0.0") 31 | let response = AppStoreSearchResponse(results: [result1, result2]) 32 | let networkService = UpdateServiceNetworkMock() 33 | networkService.performHandler = { _, completion in 34 | completion(.success(response)) 35 | return NetworkServiceTaskMock() 36 | } 37 | 38 | var didDetectUpdates = false 39 | let service = UpdateService(networkService: networkService, bundle: bundle) 40 | service.detectUpdates { didDetectUpdates = true } 41 | XCTAssertFalse(didDetectUpdates) 42 | } 43 | 44 | func testDetectUpdatesNetworkError() { 45 | let bundleIdentifier = "com.mttcrsp.fosdem" 46 | let bundle = UpdateServiceBundleMock(bundleIdentifier: bundleIdentifier, bundleShortVersion: "1.0.0") 47 | 48 | let networkServiceError = NSError(domain: "com.mttcrsp.fosdem", code: 1) 49 | let networkService = UpdateServiceNetworkMock() 50 | networkService.performHandler = { _, completion in 51 | completion(.failure(networkServiceError)) 52 | return NetworkServiceTaskMock() 53 | } 54 | 55 | var didDetectUpdates = false 56 | let service = UpdateService(networkService: networkService, bundle: bundle) 57 | service.detectUpdates { didDetectUpdates = true } 58 | XCTAssertFalse(didDetectUpdates) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /App/UITests/ApplicationControllerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class AppliationControllerTests: XCTestCase { 4 | func testOnboarding() { 5 | let app = XCUIApplication() 6 | app.launchEnvironment = ["ENABLE_ONBOARDING": "1", "RESET_DEFAULTS": "1"] 7 | app.launch() 8 | 9 | let continueButton = app.buttons["continue"] 10 | continueButton.tap() 11 | XCTAssert(app.searchButton.exists) 12 | 13 | app.terminate() 14 | app.launchEnvironment = ["ENABLE_ONBOARDING": "1"] 15 | app.launch() 16 | XCTAssert(app.searchButton.exists) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /App/UITests/Resources/test.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mttcrsp/fosdem/f7b0d75009871f7ffa67d8b9f3a8e31467c9d793/App/UITests/Resources/test.mp4 -------------------------------------------------------------------------------- /App/UITests/ScreenshotTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class ScreenshotTests: XCTestCase { 4 | func testScreenshots() throws { 5 | let device = try XCTUnwrap(ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"]) 6 | 7 | let app = XCUIApplication() 8 | app.launchEnvironment = [ 9 | "SET_FAVORITE_EVENTS": "10746,9662,9743,9457,10213,9189", 10 | "SET_FAVORITE_TRACKS": "LLVM,Containers", 11 | ] 12 | app.launch() 13 | 14 | let takeScreenshot: (String) -> Void = { name in 15 | let screenshot = app.screenshot() 16 | let attachment = XCTAttachment(screenshot: screenshot, quality: .original) 17 | attachment.lifetime = .keepAlways 18 | attachment.name = "\(device)_\(name)" 19 | self.add(attachment) 20 | } 21 | 22 | runActivity(named: "Search") { 23 | app.searchButton.tap() 24 | takeScreenshot("1_search") 25 | } 26 | 27 | runActivity(named: "Agenda") { 28 | app.agendaButton.tap() 29 | Thread.sleep(forTimeInterval: 1) // hide scroll indicator 30 | takeScreenshot("2_agenda") 31 | } 32 | 33 | runActivity(named: "Map") { 34 | app.mapButton.tap() 35 | app.niceBuildingView.tap() 36 | wait { app.blueprintsContainer.exists } 37 | 38 | if device.lowercased().contains("ipad") { 39 | let vector1 = CGVector(dx: 0.5, dy: 0.5) 40 | let vector2 = CGVector(dx: 0.57, dy: 0.55) 41 | let coordinate1 = app.coordinate(withNormalizedOffset: vector1) 42 | let coordinate2 = app.coordinate(withNormalizedOffset: vector2) 43 | coordinate1.press(forDuration: 0, thenDragTo: coordinate2) 44 | } 45 | 46 | takeScreenshot("3_map") 47 | } 48 | 49 | runActivity(named: "More") { 50 | app.moreButton.tap() 51 | Thread.sleep(forTimeInterval: 1) // hide scroll indicator 52 | takeScreenshot("4_more") 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /App/UITests/TraitsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class TraitsTests: XCTestCase { 4 | func testUserInterfaceStyle() { 5 | let app = XCUIApplication() 6 | app.launch() 7 | app.searchButton.tap() 8 | 9 | runActivity(named: "Show event") { 10 | app.day1TrackCell.tap() 11 | wait { app.trackTable.exists } 12 | app.day1TrackEventCell.tap() 13 | } 14 | 15 | runActivity(named: "Show fullscreen blueprints") { 16 | app.mapButton.tap() 17 | wait { app.buildingView.exists } 18 | app.buildingView.tap() 19 | app.blueprintsContainer.tap() 20 | } 21 | 22 | let settings = XCUIApplication.settings 23 | 24 | runActivity(named: "Enable dark mode") { 25 | settings.launch() 26 | settings.swipeUp() 27 | settings.cells["Developer"].tap() 28 | settings.switches["Dark Appearance"].tap() 29 | } 30 | 31 | runActivity(named: "Dismiss everything") { 32 | app.activate() 33 | app.blueprintsFullscreenDismissButton.tap() 34 | app.searchButton.tap() 35 | app.backButton.tap() 36 | app.backButton.tap() 37 | } 38 | 39 | runActivity(named: "Disable dark mode") { 40 | settings.activate() 41 | settings.switches["Dark Appearance"].tap() 42 | } 43 | } 44 | 45 | func testPreferredContentSizeCategory() { 46 | let app = XCUIApplication() 47 | app.launch() 48 | app.searchButton.tap() 49 | 50 | runActivity(named: "Present views") { 51 | app.day1TrackCell.tap() 52 | wait { app.trackTable.exists } 53 | app.day1TrackEventCell.tap() 54 | app.moreButton.tap() 55 | app.cells["history"].tap() 56 | } 57 | 58 | let settings = XCUIApplication.settings 59 | let sizeSlider = settings.sliders.firstMatch 60 | let maxSizeOffset = CGVector(dx: 1, dy: 0.5) 61 | let midSizeOffset = CGVector(dx: 0.5, dy: 0.5) 62 | let maxCoordinate = sizeSlider.coordinate(withNormalizedOffset: maxSizeOffset) 63 | let midCoordinate = sizeSlider.coordinate(withNormalizedOffset: midSizeOffset) 64 | 65 | runActivity(named: "Increase size") { 66 | settings.launch() 67 | settings.cells["Accessibility"].tap() 68 | settings.cells["Display & Text Size"].tap() 69 | settings.cells["Larger Text"].tap() 70 | maxCoordinate.tap() 71 | } 72 | 73 | runActivity(named: "Inspect views") { 74 | app.activate() 75 | app.searchButton.tap() 76 | } 77 | 78 | runActivity(named: "Decrease size") { 79 | settings.activate() 80 | midCoordinate.tap() 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /App/UITests/VideoControllerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class VideoControllerTests: XCTestCase { 4 | func testVideos() throws { 5 | let data = try XCTUnwrap(BundleDataLoader().data(forResource: "test", withExtension: "mp4")) 6 | 7 | let app = XCUIApplication() 8 | app.launchEnvironment = ["RESET_DEFAULTS": "1", "OVERRIDE_VIDEO": data.base64EncodedString()] 9 | app.launch() 10 | 11 | // WORKAROUND: UISegmented control does not support custom accessibility 12 | // identifiers for its segments 13 | let watchingButton = app.segmentedControls.buttons.element(boundBy: 0) 14 | let watchedButton = app.segmentedControls.buttons.element(boundBy: 1) 15 | 16 | runActivity(named: "Watching updates") { 17 | app.searchButton.tap() 18 | app.day1TrackCell.tap() 19 | app.day1TrackEventCell.tap() 20 | app.buttons["play"].tap() 21 | app.buttons["Done"].tap() 22 | app.moreButton.tap() 23 | app.cells["video"].tap() 24 | XCTAssert(app.day1TrackEventCell.exists) 25 | } 26 | 27 | runActivity(named: "Watched updates") { 28 | watchedButton.tap() 29 | XCTAssert(app.staticTexts["background_title"].exists) 30 | XCTAssert(app.staticTexts["background_message"].exists) 31 | 32 | watchingButton.tap() 33 | app.day1TrackEventCell.tap() 34 | app.buttons["resume"].tap() 35 | wait(timeout: 15) { app.backButton.exists } 36 | 37 | app.backButton.tap() 38 | XCTAssert(app.staticTexts["background_title"].exists) 39 | XCTAssert(app.staticTexts["background_message"].exists) 40 | 41 | watchedButton.tap() 42 | XCTAssert(app.day1TrackEventCell.exists) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /App/UITests/XCTNSPredicateExpectation+Extensions.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTNSPredicateExpectation { 4 | convenience init(predicate block: @escaping () -> Bool) { 5 | let predicate = NSPredicate(block: { _, _ in block() }) 6 | self.init(predicate: predicate, object: nil) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /App/UITests/XCTestCase+Extensions.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | func runActivity(named: String, block: () -> Void) { 5 | XCTContext.runActivity(named: named) { _ in block() } 6 | } 7 | 8 | func wait(file: StaticString = #file, line: UInt = #line, timeout: TimeInterval = 10, for predicate: @escaping () -> Bool) { 9 | let expectation = XCTNSPredicateExpectation(predicate: predicate) 10 | if XCTWaiter().wait(for: [expectation], timeout: timeout) != .completed { 11 | XCTFail("Expectation failed", file: file, line: line) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /App/UITests/XCUIApplication+Extensions.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCUIApplication { 4 | static var settings: XCUIApplication { 5 | XCUIApplication(bundleIdentifier: "com.apple.Preferences") 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /App/UITests/XCUIElement+Extensions.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCUIElement { 4 | var backButton: XCUIElement { 5 | navigationBars.buttons.firstMatch 6 | } 7 | 8 | // WORKAROUND: UITableView does not provide APIs to configure the 9 | // accessibility identifier of swipe actions. The only way to identify a given 10 | // action is to either use its localized accessibility value (will break when 11 | // changing locale) or attempt to guess the button to tap based on the 12 | // elements hierarchy structure (e.g. `buttons[buttons.count - 1]`). I decided 13 | // to go with the first option, routing all calls to this method to simplify 14 | // refactoring later on. 15 | func tapTrailingAction(withIdentifier identifier: String) { 16 | swipeLeft() 17 | buttons[identifier].tap() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /App/UITests/YearControllerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class YearControllerTests: XCTestCase { 4 | private var app: XCUIApplication! 5 | 6 | override func setUp() { 7 | super.setUp() 8 | app = XCUIApplication() 9 | app.launch() 10 | app.moreButton.tap() 11 | app.yearsCell.tap() 12 | app.yearCell.tap() 13 | } 14 | 15 | func testTracks() { 16 | wait { self.app.trackCell.exists } 17 | app.trackCell.tap() 18 | app.staticTexts["Welcome to the Ada DevRoom"].tap() 19 | app.backButton.tap() 20 | app.backButton.tap() 21 | } 22 | 23 | func testSearch() { 24 | let cancelButton = app.navigationBars.buttons.firstMatch 25 | let searchField = app.searchFields.firstMatch 26 | 27 | wait { searchField.exists } 28 | searchField.tap() 29 | searchField.typeText("FOSDEM") 30 | app.staticTexts["Welcome to FOSDEM 2019"].tap() 31 | app.backButton.tap() 32 | cancelButton.tap() 33 | XCTAssert(app.navigationBars["2019"].exists) 34 | } 35 | } 36 | 37 | private extension XCUIApplication { 38 | var yearsCell: XCUIElement { 39 | cells["years"] 40 | } 41 | 42 | var yearCell: XCUIElement { 43 | cells["2019"] 44 | } 45 | 46 | var trackCell: XCUIElement { 47 | cells["Ada"] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "mockolo" 2 | brew "swiftformat" 3 | brew "xcbeautify" 4 | brew "xcodegen" 5 | brew "swiftgen" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Matteo Crespi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export PATH := /opt/homebrew/bin/:$(PATH) 2 | 3 | generate_project: 4 | xcodegen 5 | 6 | run_mockolo: 7 | if [ ! -f "App/Mocks" ]; then \ 8 | mkdir App/Mocks/ && \ 9 | touch App/Mocks/Mockolo.swift; \ 10 | fi; \ 11 | mockolo \ 12 | --sourcedirs App/Sources/ \ 13 | --destination App/Mocks/Mockolo.swift \ 14 | --testable-imports Fosdem \ 15 | --mock-final \ 16 | --enable-args-history 17 | 18 | run_swiftformat:: 19 | if [ -z "$(IS_CI)" ]; then \ 20 | swiftformat .; \ 21 | fi 22 | 23 | run_swiftgen: 24 | if [ ! -f "App/Sources/Derived" ]; then \ 25 | mkdir App/Sources/Derived; \ 26 | fi; \ 27 | swiftgen run strings App/Resources/* \ 28 | -t structured-swift5 \ 29 | -o App/Sources/Derived/Strings.swift 30 | swiftgen run xcassets App/Resources/* \ 31 | -t swift5 \ 32 | -o App/Sources/Derived/Assets.swift 33 | 34 | test: 35 | xcodebuild \ 36 | -scheme FOSDEM \ 37 | -destination 'platform=iOS Simulator,OS=16.2,name=iPhone 8 Plus' \ 38 | test | xcbeautify 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > The name FOSDEM and the gear logo are registered trademarks of FOSDEM VZW 2 | 3 | [![MIT license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://raw.githubusercontent.com/wikimedia/wikipedia-ios/develop/LICENSE.txt) 4 | [![codecov](https://codecov.io/gh/mttcrsp/fosdem/branch/main/graph/badge.svg?token=fKaqxmEQC7)](https://codecov.io/gh/mttcrsp/fosdem) 5 | 6 | # Fosdem.app 7 | 8 | [![Available on the App Store](http://cl.ly/WouG/Download_on_the_App_Store_Badge_US-UK_135x40.svg)](https://itunes.apple.com/it/app/id1513719757) 9 | 10 | FOSDEM.app is a full featured iOS application that lets you organise your visit to the FOSDEM conference. 11 | 12 | - Check the schedule for each track and search for events 13 | - Watch videos from this and previous years of the conference 14 | - Navigate around the campus with a map and blueprints for each building 15 | - Build up an agenda with your favorite events to quickly see what is coming up next 16 | - Get access to slides and all material provided by speakers 17 | 18 | The app is compatible with iOS 11+ iPhone/iPad devices and Mac computers with Apple silicon, and provides support for dynamic type, VoiceOver and Dark Mode. 19 | 20 | ## Contributing 21 | 22 | Contributions in the form of code, issues and feature requests are welcome! To get started with development, run `brew bundle && make generate_project`. 23 | 24 | ## Acknowledgements 25 | 26 | Fosdem.app is powered by the following open source projects: [GRDB.swift](https://github.com/groue/GRDB.swift), [Mockolo](https://github.com/uber/mockolo), [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing), [Periphery](https://github.com/peripheryapp/periphery), [SwiftFormat](https://github.com/nicklockwood/SwiftFormat), [xcbeautify](https://github.com/tuist/xcbeautify), [XcodeGen](https://github.com/yonaskolb/XcodeGen). -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "Tests/" 3 | - "SnapshotTests/" 4 | - "UITests/" 5 | coverage: 6 | status: 7 | patch: off 8 | project: off 9 | -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | name: FOSDEM 2 | 3 | packages: 4 | GRDB: 5 | url: https://github.com/groue/GRDB.swift 6 | exactVersion: 6.29.3 7 | SnapshotTesting: 8 | url: https://github.com/pointfreeco/swift-snapshot-testing.git 9 | exactVersion: 1.17.6 10 | 11 | options: 12 | bundleIdPrefix: com.mttcrsp.fosdem 13 | createIntermediateGroups: true 14 | deploymentTarget: 15 | iOS: "14.0" 16 | macOS: '10.13' 17 | transitivelyLinkDependencies: true 18 | preGenCommand: make run_mockolo run_swiftgen 19 | 20 | settings: 21 | DEVELOPMENT_TEAM: "3CM92FF2C5" 22 | 23 | targets: 24 | FOSDEM: 25 | type: application 26 | platform: iOS 27 | sources: 28 | - App/Sources 29 | - App/Resources 30 | dependencies: 31 | - package: GRDB 32 | settings: 33 | CODE_SIGN_ENTITLEMENTS: App/Resources/FOSDEM.entitlements 34 | CURRENT_PROJECT_VERSION: 5 35 | INFOPLIST_FILE: App/Resources/Info.plist 36 | MARKETING_VERSION: 1.6.0 37 | PRODUCT_BUNDLE_IDENTIFIER: com.mttcrsp.fosdem 38 | PRODUCT_MODULE_NAME: Fosdem 39 | scheme: 40 | environmentVariables: 41 | ENABLE_ONBOARDING: "1" 42 | ENABLE_SCHEDULE_UPDATES: "1" 43 | gatherCoverageData: true 44 | testTargets: 45 | - name: Tests 46 | randomExecutionOrder: true 47 | - name: SnapshotTests 48 | randomExecutionOrder: true 49 | - name: UITests 50 | randomExecutionOrder: true 51 | 52 | Tests: 53 | type: bundle.unit-test 54 | platform: iOS 55 | sources: 56 | - App/Tests 57 | - App/Mocks 58 | - App/Resources/Buildings 59 | dependencies: 60 | - target: FOSDEM 61 | settings: 62 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: "$(inherited)" 63 | GENERATE_INFOPLIST_FILE: YES 64 | TEST_HOST: "$(BUILT_PRODUCTS_DIR)/FOSDEM.app/FOSDEM" 65 | 66 | SnapshotTests: 67 | type: bundle.unit-test 68 | platform: iOS 69 | sources: 70 | - path: App/SnapshotTests 71 | excludes: 72 | - __Snapshots__ 73 | - App/Mocks 74 | dependencies: 75 | - target: FOSDEM 76 | - package: SnapshotTesting 77 | settings: 78 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: "$(inherited)" 79 | GENERATE_INFOPLIST_FILE: YES 80 | TEST_HOST: "$(BUILT_PRODUCTS_DIR)/FOSDEM.app/FOSDEM" 81 | 82 | UITests: 83 | type: bundle.ui-testing 84 | platform: iOS 85 | sources: 86 | - App/UITests 87 | - App/Tests/BundleDataLoader.swift 88 | dependencies: 89 | - target: FOSDEM 90 | settings: 91 | GENERATE_INFOPLIST_FILE: YES 92 | -------------------------------------------------------------------------------- /take-screenshots: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | declare -a devices=( \ 4 | "iPhone 8" \ 5 | "iPhone 8 Plus" \ 6 | "iPhone 11 Pro" \ 7 | "iPhone 11 Pro Max" \ 8 | "iPad (7th generation)" \ 9 | "iPad Pro (11-inch) (2nd generation)" \ 10 | "iPad Pro (12.9-inch) (4th generation)" \ 11 | ) 12 | 13 | for device in "${devices[@]}" 14 | do 15 | echo "Booting $device" 16 | xcrun simctl boot "$device" 17 | echo "Configuring $device" 18 | xcrun simctl status_bar "$device" override \ 19 | --batteryLevel 100 \ 20 | --cellularBars 4 \ 21 | --time "9:41" 22 | done 23 | 24 | xcodebuild \ 25 | -scheme FOSDEM \ 26 | -sdk iphonesimulator \ 27 | -derivedDataPath ./Build/ \ 28 | -destination "name=iPhone 8" \ 29 | -destination "name=iPhone 8 Plus" \ 30 | -destination "name=iPhone 11 Pro" \ 31 | -destination "name=iPhone 11 Pro Max" \ 32 | -destination "name=iPad (7th generation)" \ 33 | -destination "name=iPad Pro (11-inch) (2nd generation)" \ 34 | -destination "name=iPad Pro (12.9-inch) (4th generation)" \ 35 | -only-testing UITests/ScreenshotTests/testScreenshots \ 36 | test 37 | 38 | find ./Build/Logs/Test -name \*.xcresult -maxdepth 1 -exec xcparse screenshots {} ./Screenshots \; 39 | 40 | for device in "${devices[@]}" 41 | do 42 | echo "Shutting down $device" 43 | xcrun simctl shutdown "$device" 44 | done 45 | --------------------------------------------------------------------------------