├── .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 |
5 |
6 | Direction: From Rogier (near Gare du Nord) towards Boondael Gare
7 | Stop: ULB
8 |
9 |
10 |
11 |
12 |
13 | Direction: From De Brouckère in central Brussels towards Delta
14 | Stop: ULB
15 |
16 |
17 |
18 |
19 |
20 | Direction: From ADEPS towards ULB
21 | Stop: ULB
22 |
23 |
24 |
25 |
26 | Direction: From Louise towards Roodebeek
27 | Stop: ULB
28 |
--------------------------------------------------------------------------------
/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 |
10 | Linda Calders
11 | Quint Daenen
12 | Gerry Demaret
13 | Julien Detroz
14 | Pieter De Praetere
15 | Michael De Swert
16 | Philippe De Swert
17 | Egbert De Vries
18 | Geert Goossens
19 | Jan Gyselinck
20 | Richard Hartmann
21 | Alasdair Kergon
22 | Vasil Kolev
23 | Jan-Frederik Martens
24 | Marthe Parada Delgado
25 | Thomas Perale
26 | Stella Rouzi
27 | Sebastian Schauenburg
28 | Jonas Scheers
29 | Kristian Schuhmacher
30 | Johan van Selst
31 | Wouter Simons
32 | Ieva Supjeva
33 | Miguel Terol Espino
34 | Emanuil Tolev
35 | Johan Van de Wauw
36 | Mark Van den Borre
37 | Peter Van Eynde
38 | Wouter Verhelst
39 | Koen Vervloesem
40 | The many volunteers who help out during the weekend
41 | ...
42 |
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 |
3 | Take the train to Brussels
4 | Get off at Brussels Central station
5 | Take Bus 71 , direction Delta
6 | Follow the instructions in the Bus section.
7 |
--------------------------------------------------------------------------------
/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 |
3 | Take the Metro line 2 , direction Elisabeth
4 | Get off at station Louise
5 | Take the Tram 8 , direction Roodebeek
6 |
7 | From Brussels Central ("Bruxelles Central", "Brussel Centraal" or "Gare Centrale") station (about 20 minutes):
8 |
9 | Take Bus 71, direction Delta
10 |
11 | From Brussels North ("Bruxelles Nord" or "Gare du Nord") station (about 30 minutes):
12 |
13 | Take Tram 25 , direction Boondael Gare
14 | Follow the instructions above.
15 |
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 | 2014-09-24T14:55:37Z
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 | [](https://raw.githubusercontent.com/wikimedia/wikipedia-ios/develop/LICENSE.txt)
4 | [](https://codecov.io/gh/mttcrsp/fosdem)
5 |
6 | # Fosdem.app
7 |
8 | [](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 |
--------------------------------------------------------------------------------