MetroBuddy is an iOS app that allows you to track the balance of your transit card, with a primary focus on New York's MetroCard. As a New Yorker (even though 2020 is not exactly the year of subway-riding), seeing this message is rather frustrating...
I built MetroBuddy to alleviate this pain, and improve my SwiftUI and app architecting skills. The app is available for free on the App Store.
- The app must not get in the way of the user's commute
- No onboarding/account required
- No cellular connection required
- Fast and straightforward interactions
- Turnstiles aren't always accessible, so the app must be
- Property tag views for accessibility
- Design with Dynamic Type and color contrast in mind
- Keep design simple
These are the main screens of the app:
Onboarding | Idle State | Expiration Date Picker |
---|---|---|
The information is structured in a way that satisfies both of the core principles: obvious and accessible. The linear data layout works well with screen readers. The use of a larger font for the balance makes it easier for riders to know if they need to reload their MetroCard before swiping while they are running down the station's stairs.
As a public transit enthusiast, swiping my MetroCard to enter the subway brings me joy. I wanted to replicate this enthusiasm in the app by making it playful and "obviously metro".
- The card's balance is displayed inside a view that resembles a MetroCard
- You can swipe the card to the left to record one swipe
- When the balance is insufficient to record a swipe, a toast with a monospaced font appears to display the same message as the turnstile
Since the app will most likely be used just before or right after the user enters the subway, a dark design felt like a natural decision to better accomodates the subway's minimally lit environment.
- The
MetroKit
framework contains the core building blocks of the app:- The app's Core Data models
- The
MetroCardDataStore
protocol that exposes Combine publishers for getting and updating the card - The
UserPreferences
protocol that provides a type-safe wrapper around UserDefaults - Helpers to load and configure persistent containers
- The main
App
target- Uses the
MVVM
pattern:ObservableObject
view models get the data fromMetroKit
, compute the contents of the view and publish them- SwiftUI views subscribe to their view model through
EnvironmentObject
and forwards them the user's input
- Organized by screens (root/error/main) which each have their own components
- Defines shared components to be reused by multiple views and helpers
- Uses the
The app's Core Data setup is heavily inspired by CareKit's approach. Consumers of the data makes requests and submits updates through a single store, which returns static snapshots in the form of a struct
based on the data of the managed object.
To facilitate accessing the data of this snapshot, we wrap it inside an ObjectReference
object, which contains the managed object's identifier that can only be used internally by MetroKit
without being exposed to consumers of the store. Members of the snapshot are accessible directly from the
object reference, through a dynamic member lookup subscript. For example, these two calls are equivalent:
let card: ObjectReference<MetroCard> = ... // get the card from the store
card.snapshot.balance == card.balance // true
This is the overview of the state of testing:
MetroKit
stores and models are unit-tested- The app's core flows are UI-tested
- The app's view models should be unit-tested
- The app should be tested and deployed continously
To facilitate unit-testing, the project contains a MetroTesting
framework, that defines mocks for the data store and preferences. To configure UI tests, we define scenarios, which are types that have a method that creates a mock. Scenarios are usually scoped to a single use case, e.g. the "new install" scenario or "returning user" scenario. The UI tests can pass the name of the scenario class to the XCUIApplication
's launch arguments. The app will attempt to parse the scenario's class name and get the mock store from it. The MetroTesting
framework and this injection logic are only enabled on debug builds. The framework is not included in the release build.
UI tests also use the page-object pattern, which is a way to decouple the automation logic from test assertions. The app's screens are organized into pages that use XCUIApplication
to get properties from elements and interact with them.
To record screenshots for the App Store, we use snapshot from fastlane.
Contributions, feedback and suggestions are more than welcome! Please read the Contribution Guidelines and License before. While I am mostly looking for help in the following areas, all contributions are welcome and appreciated:
- Documentation improvements
- Accessibility issues
- Design review
- App Architecture feedback
I'm Alexis Aubry-Akers, an iOS engineer based in NYC. You can learn more about me on my website.
- Twitter: @_alexaubry
- Email: hi@alexisonline.dev
The source code and documentation are licensed under the MIT license. Source code is defined as compilable text files (ending in .swift
, .plist
, .xcdatamodel
, .xcodeproj
). However, designs, flows and assets are copyrighted and may not be redistributed without permission. You are not permitted to redistribute copies the application on the App Store.
Please note that the app is not affiliated with New York's MTA in any way.
The app uses the following third party libraries: