← Back to portfolio

INDEPENDENT PROJECT · NATIVE iOS

Award
Calculator.

Turning complex flight rules into clear decisions.

I designed and built an iPhone and iPad app that explains how an Aeroplan reward itinerary fits together—from routing and points to connection times and cached seat availability.

TestFlight 1.0.0 (9) · Released October 27, 2026

Plan the route
Understand the result
My role
Product design · iOS engineering · testing
Built with
Swift · SwiftUI · MapKit · URLSession · Keychain
Delivery
iPhone + iPad · iOS 17+ · TestFlight
12flights per itinerary
6bounds per booking
3native interface languages
43,216offline connection rules¹

01 / THE PROBLEM

A route is a line.
An itinerary is a set of constraints.

A plausible-looking route can still fail a distance rule, require multiple pricing bounds, contain an impossible connection, or have no confirmed reward space. I wanted the app to explain those distinctions instead of hiding them behind a single points number.

01

Make the rules inspectable

Show the chosen bounds, their distances, stopovers, fare choices and reasons for splitting.

02

Keep uncertainty visible

Separate a published flight, cached reward availability, a verified connection and an unresolved rule.

03

Stay usable while editing

Let people change dates, cabin codes, layouts and languages without losing a draft or applying an old response.

02 / THE EXPERIENCE

Plan. Inspect.
Decide with context.

The map gives the trip a shape. The editor makes details precise. The result explains what the rules actually allow.

01

Build the itinerary

Search by flight or route, keep local airport times, select cabins and booking codes, and retain manual overrides.

02

Read the explanation

See the points estimate broken into bounds, with stopovers, fare choices and routing decisions alongside it.

03

Know what is verified

A confirmed short connection blocks the total. Unresolved missing or conflicting information stays explicitly unverified.

Actual native app screenshots from build 9. Select any image to inspect it at a larger size.

03 / REAL TESTED ROUTES

Examples that shaped
the engine.

A bound is a separately priced one-way portion of a trip. Explore saved development scenarios and regression fixtures with recorded outcomes.

Fixed-chart estimates for the recorded cabin and fare choices. The analysis groups a supplied route; it does not search for the cheapest flight or promise current award availability.

Why a greedy split was wrong

The distance limit depends on the endpoints of the complete bound. A partial route can fail, then become valid once a later destination changes the direct-distance baseline. Treating every failed prefix as a permanent split produced the wrong partition.

I replaced that decision with suffix dynamic programming: evaluate each complete candidate of up to six flights, minimize the number of bounds, and prefer a longer earlier bound when counts tie. The ten-flight regression changed from 375,000 to the intended 310,000 points.

This is a routing policy, not a minimum-points optimizer. A different valid four-bound partition totals 290,000 points; the specified tie-break intentionally selects the 310,000-point arrangement.

The partition decision

01Start from the end of the supplied itinerary.
02Try complete candidates of 1–6 flights.
03Validate routing, geography and stopover constraints.
04Choose the candidate + suffix with the fewest bounds.
05Break ties with the longer earlier candidate.
06Reconstruct bounds and explain every split.

INSIDE THE ROUTING ENGINE

The decision, in Swift.

A small excerpt of the current partitioning implementation in Xcode. It evaluates complete candidates, retains the best suffix, and keeps invalid single flights visible for diagnosis instead of dropping them.

Development view captured October 29, 2026. The same original Swift excerpt appears in both language versions.

04 / ENGINEERING

A testable core.
A native interface.

I separated the domain model, on-device state, provider integrations and presentation so a screen change does not become a pricing-rule change.

01

SwiftUI presentation

MapKit globe, editor, result views and charts. Layout and language are presentation choices.

02

On-device state

A MainActor store owns drafts, saved itineraries, travelers and fare selections.

03

Deterministic analysis

Swift models analyze complete bounds, chart pricing, stopovers and connection requirements.

04

External context

Separate service actors provide schedules and cached reward data without replacing the local rules.

01

Rule matching with an honest “unknown”

Connection rules can depend on airport pairs, operating or marketing carriers, terminals, flight-number ranges and dates. Carrier-specific selectors take precedence over station defaults.

  • 43,216 rules across 7,844 airports¹, with imported conditions retained.
  • Confirmed insufficient time blocks the itinerary total—even across bound boundaries.
  • Unresolved selectors, conflicts and possible prohibitions stay unverified; a complete numeric envelope can still prove a pass or failure.
02

Availability tied to the selected code

Build 9 distinguishes cabin-level availability from the actual Air Canada booking code selected in the editor. P inventory must not appear as I inventory.

  • Changing the code invalidates the old result immediately.
  • An exact reported count stays attached to its own code.
  • Inferred higher-code availability is labeled without inventing an exact seat count.
03

Failures are part of the data model

FlightAware schedules, Seats.aero availability and Landline timetables have separate service actors. Identical in-flight requests are shared; manual input remains usable when lookups fail.

  • 15-minute cache freshness, with stale responses explicitly marked.
  • Missing matches, authentication failures, rate limits and partial results are distinct states.
  • Personal API keys use device-only Keychain storage.
04

Drafts survive changing layouts

Each editor opening receives a fresh session ID, stable across resizing. Old callbacks cannot mutate a newly reopened editor—even in an A → B → A sequence.

  • The map stays mounted while editing, avoiding a globe reload on return.
  • Manual schedule overrides survive refreshes.
  • Adaptive layout fixes preserve state rather than resetting the entire view.
05

Precision at the decision boundary

The engine sums great-circle distances in floating point and compares the complete bound against twice its endpoint distance. Rounding belongs at display and chart lookup—not inside the feasibility check.

  • Exactly 2× and just-over-the-limit cases have distinct regression fixtures.
  • Open gaps and ground transfers do not become invented flight mileage.
  • Every supplied flight must appear exactly once in the reconstructed partition.
06

One price resolver, explicit missing values

The displayed charts and the itinerary engine share one resolver. Approved imported cells take precedence; explicit blanks stay blank. An unapproved range never becomes a guessed midpoint.

  • Each bound uses its highest cabin and an eligible fare brand.
  • Stopover charges scale with the number of travelers.
  • Unknown pricing prevents a complete total instead of silently becoming zero.

DESIGNING FOR AVAILABLE SPACE

One product.
Different ways to work.

The phone uses a movable flight panel over the map. On iPad, a floating flight island leaves the globe visible. Results and charts choose columns from their actual presented width, and large text returns them to a single column.

I also implemented named panel actions, adjustable controls, Reduce Motion and Reduce Transparency behavior. Selected layouts were checked in Simulator; this is not a claim of exhaustive accessibility certification.

¹ The offline database is a dated snapshot of station standards and collected carrier exceptions, not exhaustive worldwide coverage. Runtime data is JSON; a separate SQLite export supports research and validation.

05 / THE INTERFACE, UP CLOSE

Built around
the next decision.

Real build-9 screens. English and Chinese show matching views; switching this page’s language changes every app screenshot as well.

06 / VALIDATION & DELIVERY

Evidence behind
the interface.

I tested the domain rules separately from the UI, checked service responses and cache behavior with fixtures, then verified selected native flows in Simulator and shipped a signed build through TestFlight.

Three native languages share stable business values and saved itinerary data. Build 9 contains 816 keys in each language catalog; localization checks cover missing keys and formatting.

The figures here are separate, scoped check suites recorded for build 9. They are not added into a headline total, and automated layout checks do not substitute for every physical-device or accessibility test.

Native booking-code availability877
Language coverage & formatting5,144
Adaptive viewport helpers5,410
Panel, swipe & island helpers263
What this project demonstrates: translating a messy domain into testable rules, designing around incomplete external data, and carrying a native product from interaction design to a verified release.

LET’S TALK ABOUT THE WORK

Interested in how I build?

I’m happy to discuss the routing engine, the UI decisions, or engineering opportunities. Public TestFlight testing is also open—contact me for an invitation.

Independent personal project, not affiliated with Air Canada or Aeroplan. Fixed-chart estimates exclude taxes and dynamic pricing; schedules and cached availability are not booking guarantees.