{"id":18974569,"url":"https://github.com/boyvanamstel/farlake","last_synced_at":"2025-10-11T22:53:24.788Z","repository":{"id":138921374,"uuid":"277054628","full_name":"boyvanamstel/Farlake","owner":"boyvanamstel","description":"👨‍🎨 Catalyst app that uses MVVM, Coordinators, UIKit, SwiftUI, Combine, NSCache, URLCache and more to display paintings by Johannes Vermeer made available through the Rijksmuseum API.","archived":false,"fork":false,"pushed_at":"2020-07-25T20:37:48.000Z","size":4785,"stargazers_count":6,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-13T09:45:46.713Z","etag":null,"topics":["catalyst","combine","coordinator","ios","ipados","macos","mvvm","nscache","swift","swiftui","uicollectionviewcompositionallayout","uicollectionviewdiffabledatasource","uiwindowscenedelegate","urlcache"],"latest_commit_sha":null,"homepage":"https://hire.boy.sh","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/boyvanamstel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-07-04T06:41:22.000Z","updated_at":"2022-10-07T12:08:11.000Z","dependencies_parsed_at":"2023-03-15T04:01:03.296Z","dependency_job_id":null,"html_url":"https://github.com/boyvanamstel/Farlake","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/boyvanamstel/Farlake","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boyvanamstel%2FFarlake","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boyvanamstel%2FFarlake/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boyvanamstel%2FFarlake/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boyvanamstel%2FFarlake/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/boyvanamstel","download_url":"https://codeload.github.com/boyvanamstel/Farlake/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boyvanamstel%2FFarlake/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261906776,"owners_count":23228348,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["catalyst","combine","coordinator","ios","ipados","macos","mvvm","nscache","swift","swiftui","uicollectionviewcompositionallayout","uicollectionviewdiffabledatasource","uiwindowscenedelegate","urlcache"],"created_at":"2024-11-08T15:15:27.422Z","updated_at":"2025-10-11T22:53:19.767Z","avatar_url":"https://github.com/boyvanamstel.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\u003cimg src=\"Screenshots/farlake-macos_512.png\" width=\"256\" height=\"256\"\u003e\u003c/div\u003e\n\n# Farlake\n\n![Supported platforms are iOS, iPadOS and macOS](https://img.shields.io/badge/platform-iOS%20|%20iPadOS%20|%20macOS-blue) [![Build Status](https://app.bitrise.io/app/30371fd65c43253b/status.svg?token=TiN_VOcGo6u6t3B2HCX3vA)](https://app.bitrise.io/app/30371fd65c43253b)\n\nFarlake is a small demo application that ties together Coordinators, MVVM, Catalyst, SwiftUI, UICollectionViewDiffableDataSource, UICollectionViewCompositionalLayout, NSCache, URLCache and a few other things to display art by Johannes Vermeer.\n\nThe content is made available by [Het Rijksmuseum](https://rijksmuseum.nl) through [their API](https://data.rijksmuseum.nl).\n\n## Setup\n\n### Clone the project\n\n```\ngit clone git@github.com:boyvanamstel/Farlake.git\ncd Farlake\n```\n\n### Bootstrap\n\nEither build the project once, or run the included script to create `Farlake/Shared/Configuration/SecretConstants.swift`. It holds the Rijksmuseum API key that you'll need to supply.\n\n```\n./script/bootstrap\n```\n\nThe script copies `SecretConstants-example.swift` to `SecretConstants.swift`. This allows me to open source the project without including any sensitive data.\n\n### API key\n\nVisit [the Rijksmuseum API page](https://data.rijksmuseum.nl/object-metadata/api/) to register for an API key. Add it to `Farlake/Shared/Configuration/SecretConstants.swift`:\n\n```swift\nstruct SecretConstants {\n    static let apiKey = \"[your key]\"\n}\n```\n\n### Consistency\n\n[`SwiftLint`](https://github.com/realm/SwiftLint) is used to enforce Swift style and conventions.\n\nMost classes are marked as `final` to discourage subclassing and get a tiny performance win.\n\n## Overview\n\nThis is broadly how the project is structured:\n\n```\nFarlake/\n|-- iOS/                                    *iOS specific files*\n|-- macOS/                                  *macOS specific files*\n|-- Shared/                                 *The bulk of the project*\n|   |-- Configuration/                      *Constants and secrets*\n|   |-- Models/                             *Codable models*\n|   |-- Utilities/                          *Extensions and tools*\n|   |-- Services/                           *Networking and caching*\n|   |   |-- ServicesProvider.swift          *Dependency injection*\n|   |-- Components/                         *Reusable generic (UI) components*\n|   |-- Features/                           *Self contained features of the app*\n|   |   |-- Settings/                       *Everything for Settings*\n|   |   |-- Gallery/                        *Everything for the Gallery*\n|   |   |   |-- Views/                      *Views and View Models*\n|   |   |   |-- Utilities/                  *Helpers specifically for the Gallery*\n|   |   |   |-- GalleryCoordinator.swift    *One of the child Coordinators*\n|   |-- Resources/                          *Assets, localization, Info.plist*\n|   |-- Base/                               *Base protocols*\n|   |-- System/                             *AppDelegate and UIWindowSceneDelegate*\n|   |   |-- MainSceneCoordinator.swift      *The main Coordinator*\n|   |   |-- TestCoordinator.swift           *UI Test Coordinator*\nFarlakeTests/\nFarlakeUITests/\n```\n### Startup\n\nThe `AppDelegate` retains a default set of services through `ServicesProvider.createDefaultProvider()`. These dependencies are injected into the main coordinators, which in turn pass them to any child coordinators and view models that need them.\n\nA seperate set of dependencies gets created when running UI tests. More on that later.\n\n#### Catalyst\n\nThere are two `UIWindowSceneDelegate`s, one for the main app and one for the Preferences window on Catalyst. I manually associated the `ServicesProvider` in an extension on `UISceneSession` (`UISceneSession+Helpers.swift`) to make sure both windows use the same networking and caching dependencies.\n\n## Coordinators\n\n### Main\n\n```\nFarlake/\n|-- macOS/\n|   |-- System/\n|-- Shared/\n|   |-- System/\n```\n\nThe `MainSceneCoordinator` creates a child coordinator to display the gallery. It also makes sure the thumbnail cache is persisted to disk when the app is sent to the background (or the window is closed on Catalyst).\n\n### Gallery\n\n```\nFarlake/\n|-- Shared/\n|   |-- Features/\n|   |   |-- Gallery/\n```\n\nThe `MainSceneCoordinator` loads the `GalleryViewController` which is a subclass of `UICollectionViewController`. It can also show settings on iOS and iPadOS.\n\n### Settings\n\n```\nFarlake/\n|-- macOS/\n|   |-- Features\n|   |   |-- Settings/\n|-- Shared/\n|   |-- Features/\n|   |   |-- Settings/\n```\n\nExists in two variants: `SettingsCoordinator` and `CatalystSettingsCoordinator`. The latter gets created by the `SettingsSceneDelegate` when a new window is opened on Catalyst.\n\n## Features\n\n```\nFarlake/\n|-- Shared/\n|   |-- Features/\n```\n\nAll view models use `Combine` publishers for their bindings.\n\n### Gallery\n\nThe gallery collection view uses a compositional layout (`UICollectionViewLayout+Gallery.swift`) that adjusts its layout based on the size of the screen. I use a `UICollectionViewDiffableDataSource` to provide the content for the collection view. Farlake is definitely not pushing the envelope on diffable content, but the API is very convenient.\n\nErrors are presented as alerts. The main benefit is that they get presented as sheets on Catalyst.\n\nThe navigation view is hidden on Catalyst.\n\n### Settings\n\nThe settings view is built using `SwiftUI`. This is where the changes to Catalyst announced at WWDC20 would come in handy.\n\n## Networking\n\n```\nFarlake/\n|-- Shared/\n|   |-- Services/\n|   |   |-- Networking/\n```\n\nI use a base `NetworkService` that is then adopted by the `RijksmuseumNetworkService` and `ImageFetcher` to retrieve their `Resource\u003cObject\u003e`s. The `NetworkService` can be extended by complying to various protocols like `URLCaching`. More on that later.\n\nResources are parsed into `Decodable` models.\n\n## Caching\n\n```\nFarlake/\n|-- Shared/\n|   |-- Services/\n|   |   |-- Cache/\n```\n\nThis was an interesting part to work on. The API layer and the image fetcher both rely on standard `URLSession`s with an aggressive caching configuration.\n\n### `URLCache`\n\n```swift\n// Farlake/Shared/Services/Networking/Images/ImageFetcher.swift\n\nlet configuration: URLSessionConfiguration = .default\nconfiguration.requestCachePolicy = .returnCacheDataElseLoad\n```\n\nBy using [`.returnCacheDataElseLoad](https://developer.apple.com/documentation/foundation/nsurlrequest/cachepolicy/returncachedataelseload#), the response will always come from the first request that was made. This setting is applied to the API calls and when images are downloaded.\n\nI wasted quite some time figuring out why caching wasn't reliable, until I figured out the order of the url parameters matters:\n\n```swift\n// Farlake/Shared/Utilities/URLRequest+Helpers.swift\n\nguard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {\n    return nil\n}\ncomponents.queryItems = parameters.keys\n    .map { URLQueryItem(name: $0, value: parameters[$0]?.description) }\n    .sorted { $0.name \u003c $1.name } // Sort to ensure caching works\n```\n\n### `NSCache`\n\nTo ensure smooth scrolling the original (huge) images are resized and then stored in memory by a custom `ImageDataCache`, based on [an example by John Sundell](https://www.swiftbysundell.com/articles/caching-in-swift/).\n\nThe thumbnails are referenced by their url and the chosen thumbnail size. Multiple thumbnail sizes can be cached that way.\n\nBecause the entire cached is `Codable`, it can easily be stored on disk when the app is sent to the background and retrieved when the app launches.\n\nOne interesting implementation detail of using `NSCache` is that you'll need to make sure it doesn't get flushed when the app is sent to the background by implementing `NSDiscardableContent`.\n\n```swift\nprivate extension Cache {\n    final class Entry: NSObject, NSDiscardableContent {\n        ...\n\n        // Keep entries around after entering background state\n        // by overriding NSDiscardableContent\n        func beginContentAccess() -\u003e Bool { true }\n        func endContentAccess() {}\n        func discardContentIfPossible() {}\n        func isContentDiscarded() -\u003e Bool { false }\n    }\n}\n```\n\n### Flushing\n\nThe `URLCache` gets flushed by setting `request.cachePolicy = .reloadRevalidatingCacheData` on the resource that needs to be refreshed.\n\nThe image data cache can be cleared from the settings screen.\n\n## Responder chain\n\nThe responder chain came in handy when implementing gallery refreshing and presenting the settings screen. Both can be called from completely different locations in the app, especially on Catalyst.\n\nThere's a refresh button in the toolbar on Catalyst and a menu item that can trigger a refresh, which even has a keyboard shortcut ( ⌘ + R). They all pass their request up the responder chain until it gets picked up by an object that conforms to matching the protocol. `GalleryViewController` in this case.\n\n```swift\n// Farlake/macOS/System/MainSceneDelegate+Catalyst.swift\n\n@objc private func refreshButtonTapped(_ sender: UIButton) {\n    // Use the responder chain to find a view that can handle the action\n    UIApplication.shared.sendAction(#selector(GalleryRefreshableAction.refreshGallery), to: nil, from: sender, for: nil)\n}\n\n// Farlake/Shared/Features/Gallery/Views/GalleryViewController.swift\n\nextension GalleryViewController: GalleryRefreshableAction {\n    @objc func refreshGallery() {\n        viewModel?.updateItems()\n    }\n}\n```\n\n## Testing\n\nUI testing is managed by a dedicated `TestCoordinator` that uses  `CommandLine.argument` to put the app in the required state.\n\nVarious dependencies are replaced by mocked counterparts like the `MockRijksmuseumNetworkService`. Fixtures are used for unit testing.\n\n## Improvements\n\nThings that I didn't quite get to, or could be improved:\n\n* Localization needs to be more consistently used.\n* The `GalleryViewController` is starting to get a little heavy.\n* UI test helpers could be moved into a dedicated helper class.\n* Features: search.\n\n## Screenshots\n\n### iOS\n\n\u003cp float=\"left\"\u003e\n\u003ca href=\"Screenshots/iphone-gallery.jpg\"\u003e\u003cimg src=\"Screenshots/iphone-gallery.jpg\" width=\"260\"\u003e\u003c/a\u003e\n\u003ca href=\"Screenshots/iphone-detail.jpg\"\u003e\u003cimg src=\"Screenshots/iphone-detail.jpg\" width=\"260\"\u003e\u003c/a\u003e\n\u003ca href=\"Screenshots/iphone-settings.jpg\"\u003e\u003cimg src=\"Screenshots/iphone-settings.jpg\" width=\"260\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n### iPadOS\n\n\u003ca href=\"Screenshots/ipad-gallery.jpg\"\u003e\u003cimg src=\"Screenshots/ipad-gallery.jpg\"\u003e\u003c/a\u003e\n\u003ca href=\"Screenshots/ipad-detail.jpg\"\u003e\u003cimg src=\"Screenshots/ipad-detail.jpg\"\u003e\u003c/a\u003e\n\u003ca href=\"Screenshots/ipad-settings.jpg\"\u003e\u003cimg src=\"Screenshots/ipad-settings.jpg\"\u003e\u003c/a\u003e\n\n### macOS\n\n\u003ca href=\"Screenshots/macos.jpg\"\u003e\u003cimg src=\"Screenshots/macos.jpg\"\u003e\u003c/a\u003e\n\u003ca href=\"Screenshots/macos-detail.jpg\"\u003e\u003cimg src=\"Screenshots/macos-detail.jpg\"\u003e\u003c/a\u003e\n\n## License\n\n`MIT`, but don't submit this to the App Store as is.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboyvanamstel%2Ffarlake","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fboyvanamstel%2Ffarlake","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboyvanamstel%2Ffarlake/lists"}