Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,18 @@ jobs:
- name: Boot and configure simulator
run: |
UDID="${{ steps.sim.outputs.udid }}"
# Raise the per-host pseudo-terminal cap. xcodebuild allocates a
# fresh PTY per test launch and never reclaims them, so the macOS
# default of 127 starves late in long suites as "Lost connection to
# the application" and element-not-hittable errors.
sudo sysctl -w kern.tty.ptmx_max=999 || true
xcrun simctl boot "$UDID" || true
xcrun simctl bootstatus "$UDID" || true
xcrun simctl spawn "$UDID" defaults write -g AppleLocale en_US
# Pin the simulator timezone — runner image defaults to UTC, which
# puts CI deeper into "today" the later in the day a job fires.
xcrun simctl spawn "$UDID" defaults write -g AppleTimeZone "America/Los_Angeles"
xcrun simctl spawn "$UDID" launchctl setenv TZ "America/Los_Angeles"
xcrun simctl spawn "$UDID" defaults write com.apple.Preferences KeyboardAutocorrection -bool false
xcrun simctl spawn "$UDID" defaults write com.apple.Preferences KeyboardAutocapitalization -bool false
xcrun simctl spawn "$UDID" defaults write com.apple.Preferences KeyboardPrediction -bool false
Expand All @@ -251,12 +260,54 @@ jobs:
with:
name: derived-data-${{ matrix.ios-label }}
path: DerivedData
- name: Pre-warm app launch
run: |
UDID="${{ steps.sim.outputs.udid }}"
APP_PATH=$(find DerivedData/Build/Products -name "SF50 TOLD.app" -type d | head -n 1)
if [ -z "$APP_PATH" ]; then
echo "::warning::SF50 TOLD.app not found under derived data; skipping pre-warm"
exit 0
fi
echo "Pre-warming using $APP_PATH"
xcrun simctl install "$UDID" "$APP_PATH"
xcrun simctl launch "$UDID" codes.tim.SF50-TOLD || true
sleep 5
xcrun simctl terminate "$UDID" codes.tim.SF50-TOLD || true
- name: Run UI tests
run: |
set -o pipefail
rm -rf /tmp/ui-tests.xcresult
# `-retry-tests-on-failure -test-iterations 3` reruns only failing
# tests up to 3x. xcodebuild exits non-zero whenever any iteration
# fails (even when a later retry passed), so swallow the exit code
# and decide pass/fail by parsing the xcresult below.
xcodebuild test-without-building \
-scheme "SF50 TOLD" -testPlan "SF50 TOLD UI Tests" \
-destination "platform=iOS Simulator,id=${{ steps.sim.outputs.udid }}" \
-destination-timeout 300 \
-destination-timeout 600 \
-derivedDataPath DerivedData \
| xcbeautify --renderer github-actions
-resultBundlePath /tmp/ui-tests.xcresult \
-retry-tests-on-failure -test-iterations 3 \
2>&1 | xcbeautify --renderer github-actions || true
# Counts a test as a "real failure" only if EVERY iteration failed.
# A retried-then-passed test has both 'Failed' and 'Passed' Test Case
# nodes and the `select(... | not)` predicate drops it.
FAILED=$(xcrun xcresulttool get test-results tests \
--path /tmp/ui-tests.xcresult --format json \
| jq '[.. | objects | select(.nodeType? == "Test Case") | {name: (.nodeIdentifier // .name), result}] | group_by(.name) | map(select([.[].result] | any(. == "Passed" or . == "Skipped" or . == "Expected Failure") | not)) | length')
if [ "$FAILED" = "0" ]; then
echo "UI tests passed (after retries)"
else
echo "UI tests failed: $FAILED test(s) failed every iteration"
xcrun xcresulttool get test-results tests \
--path /tmp/ui-tests.xcresult --format json \
| jq -r '[.. | objects | select(.nodeType? == "Test Case") | {name: (.nodeIdentifier // .name), result}] | group_by(.name) | map(select([.[].result] | any(. == "Passed" or . == "Skipped" or . == "Expected Failure") | not)) | .[] | .[0].name'
exit 1
fi
- name: Upload xcresult on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: ui-tests-xcresult-${{ matrix.device }}-${{ matrix.ios-label }}
path: /tmp/ui-tests.xcresult
retention-days: 7
62 changes: 62 additions & 0 deletions .github/workflows/periphery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Periphery

on:
push:
branches: [main]
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
periphery:
name: Run Periphery
runs-on: macos-26
steps:
- name: Disable Spotlight indexing
run: |
sudo mdutil -a -i off
sudo launchctl bootout system/com.apple.metadata.mds 2>/dev/null || true
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- uses: SwiftyLab/setup-swift@latest
with:
swift-version: "6.3"
- name: Ensure iOS simulator runtime
run: |
VERSION=$(xcrun --sdk iphonesimulator --show-sdk-version)
if xcrun simctl list runtimes | grep -q "iOS ${VERSION}"; then
echo "iOS ${VERSION} runtime already installed"
else
echo "iOS ${VERSION} runtime not found, downloading..."
xcodebuild -downloadPlatform iOS
fi
- name: Create simulator
id: sim
run: |
while xcrun simctl delete "periphery-sim" 2>/dev/null; do :; done
UDID=$(xcrun simctl create "periphery-sim" com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro)
echo "udid=$UDID" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v6
- name: Generate NOTAM API config
env:
NOTAM_API_TOKEN: ${{ secrets.NOTAM_API_TOKEN }}
NOTAM_API_BASE_URL: ${{ secrets.NOTAM_API_BASE_URL }}
run: |
mkdir -p "SF50 TOLD/NOTAM"
cat > "SF50 TOLD/NOTAM/NOTAMAPIConfig.xcconfig" << EOF
NOTAM_API_TOKEN = ${NOTAM_API_TOKEN}
NOTAM_API_BASE_URL = https:/\$()/notams.fly.dev
EOF
- name: Enable macros
run: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES
- name: Install Periphery
run: brew install periphery
- name: Run Periphery
run: |
periphery scan -- \
-destination "platform=iOS Simulator,id=${{ steps.sim.outputs.udid }}" \
-destination-timeout 300
9 changes: 9 additions & 0 deletions .periphery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
project: SF50 TOLD.xcodeproj
schemes:
- SF50 TOLD
exclude_tests: true
exclude_targets:
- SF50 TOLDTests
- SF50 SharedTests
- SF50 TOLDUITests
retain_public: false
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ CHECKSUMS
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785
claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e
colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c
colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a
Expand Down
1 change: 0 additions & 1 deletion SF50 Runways/Views/SelectedAirportWidgetEntryView.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import SF50_Shared
import SwiftUI
import WidgetKit

Expand Down
2 changes: 0 additions & 2 deletions SF50 TOLD/Extensions.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import SF50_Shared

extension Array where Element: Equatable {
mutating func appendRemovingDuplicates(of newElement: Element) {
self.removeAll { $0 == newElement }
Expand Down
60 changes: 0 additions & 60 deletions SF50 TOLD/TLR/TLRModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,6 @@ struct AircraftInfo {
let emptyWeight: Measurement<UnitMass>
}

/// Wind information for TLR display. Direction is nil for variable or calm winds.
struct WindInfo {
/// Wind direction in degrees true (nil for variable/calm).
let direction: Measurement<UnitAngle>?

/// Wind speed.
let speed: Measurement<UnitSpeed>
}

/// Runway analysis results showing weight limits.
///
/// ``RunwayInfo`` captures the maximum weight that can be used for a runway
Expand Down Expand Up @@ -162,27 +153,6 @@ struct PerformanceDistance {

// MARK: - Takeoff Data Structures

/// Planned takeoff conditions for display in the TLR header.
struct TakeoffData {
/// Airport identifier.
let airport: String

/// Selected runway designator.
let plannedRunway: String

/// Outside air temperature.
let plannedOAT: Measurement<UnitTemperature>

/// Wind conditions.
let plannedWind: WindInfo

/// Altimeter setting.
let plannedQNH: Measurement<UnitPressure>

/// Planned takeoff weight.
let plannedTOW: Measurement<UnitMass>
}

/// Calculated takeoff performance for a single runway.
///
/// Contains ground run, total distance (to 50'), climb gradient, and
Expand Down Expand Up @@ -212,30 +182,6 @@ struct TakeoffPerformanceScenario {

// MARK: - Landing Data Structures

/// Planned landing conditions for display in the TLR header.
struct LandingData {
/// Airport identifier.
let airport: String

/// Selected runway designator.
let plannedRunway: String

/// Outside air temperature.
let plannedOAT: Measurement<UnitTemperature>

/// Wind conditions.
let plannedWind: WindInfo

/// Altimeter setting.
let plannedQNH: Measurement<UnitPressure>

/// Planned landing weight.
let plannedLW: Measurement<UnitMass>

/// Flap configuration description.
let configuration: String
}

/// Calculated landing performance for a single runway.
///
/// Contains Vref, landing run, landing distance (to 50'), go-around compliance,
Expand Down Expand Up @@ -279,9 +225,3 @@ struct ReportOutput<ScenarioType> {
/// Performance calculations for each scenario.
let scenarios: [ScenarioType]
}

/// Report output specialized for takeoff scenarios.
typealias TakeoffReportOutput = ReportOutput<TakeoffPerformanceScenario>

/// Report output specialized for landing scenarios.
typealias LandingReportOutput = ReportOutput<LandingPerformanceScenario>
13 changes: 0 additions & 13 deletions SF50 TOLD/Views/Helpers/View Functions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,12 @@ import SwiftUI
import UIKit

extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil,
from: nil,
for: nil
)
}

func localizedModel() -> String {
UIDevice.current.localizedModel
}
}
#else
extension View {
func hideKeyboard() {
// noop
}

func localizedModel() -> String {
"device"
}
Expand Down
1 change: 0 additions & 1 deletion SF50 TOLD/Views/Overlays/ErrorSheet.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Foundation
import SF50_Shared
import SwiftUI

struct ErrorSheet: View {
Expand Down
33 changes: 8 additions & 25 deletions SF50 TOLD/Views/Performance/Map/CoordinateCalculations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import SF50_Shared
/// Calculates the initial bearing from one coordinate to another.
///
/// Delegates to `GeoCalculations.bearing(from:to:)`.
public func bearing(
func bearing(
from start: CLLocationCoordinate2D,
to end: CLLocationCoordinate2D
) -> Measurement<UnitAngle> {
Expand All @@ -15,7 +15,7 @@ public func bearing(
/// Calculates a destination coordinate given a starting point, distance, and bearing.
///
/// Delegates to `GeoCalculations.destination(from:distance:bearing:)`.
public func destination(
func destination(
from start: CLLocationCoordinate2D,
distance: Measurement<UnitLength>,
bearing: Measurement<UnitAngle>
Expand All @@ -34,7 +34,7 @@ public func destination(
/// - thresholdCrossingHeight: TCH for approach (nil to use fallback).
/// - glidepathAngle: Glidepath angle from ILS or PAPI/VASI (nil to use fallback).
/// - Returns: Distance from threshold to touchdown zone.
public func touchdownZoneOffset(
func touchdownZoneOffset(
runwayLength: Measurement<UnitLength>,
thresholdCrossingHeight: Measurement<UnitLength>? = nil,
glidepathAngle: Measurement<UnitAngle>? = nil
Expand Down Expand Up @@ -69,7 +69,7 @@ public func touchdownZoneOffset(
/// - width: Runway width (defaults to 100 feet).
/// - Returns: Array of four coordinates representing the runway corners in clockwise order
/// starting from the left side of the threshold.
public func runwayCorners(
func runwayCorners(
threshold: CLLocationCoordinate2D,
heading: Measurement<UnitAngle>,
length: Measurement<UnitLength>,
Expand Down Expand Up @@ -101,30 +101,13 @@ public func runwayCorners(
return [thresholdLeft, thresholdRight, farEndRight, farEndLeft]
}

/// Calculates the four corner coordinates for a ground run overlay on the runway.
///
/// - Parameters:
/// - startPoint: Starting coordinate of the ground run.
/// - heading: True heading of the runway in degrees.
/// - distance: Ground run distance.
/// - width: Runway width (defaults to 100 feet).
/// - Returns: Array of four coordinates representing the ground run rectangle in clockwise order.
public func groundRunCorners(
startPoint: CLLocationCoordinate2D,
heading: Measurement<UnitAngle>,
distance: Measurement<UnitLength>,
width: Measurement<UnitLength> = .init(value: 100, unit: .feet)
) -> [CLLocationCoordinate2D] {
return runwayCorners(threshold: startPoint, heading: heading, length: distance, width: width)
}

/// A single chevron polygon with its coordinates and whether it uses primary or secondary opacity.
public struct ChevronData {
struct ChevronData {
/// The polygon coordinates for this chevron.
public let coordinates: [CLLocationCoordinate2D]
let coordinates: [CLLocationCoordinate2D]

/// Whether this chevron uses primary (true) or secondary (false) opacity.
public let isPrimary: Bool
let isPrimary: Bool
}

/// Generates tessellated chevron polygons along a path to indicate direction of travel.
Expand All @@ -140,7 +123,7 @@ public struct ChevronData {
/// - width: Width of the chevron band.
/// - depth: How far back each chevron extends (defaults to 60 feet). Also determines spacing.
/// - Returns: Array of ChevronData with coordinates and alternating primary/secondary flag.
public func generateChevrons(
func generateChevrons(
startPoint: CLLocationCoordinate2D,
heading: Measurement<UnitAngle>,
distance: Measurement<UnitLength>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import SF50_Shared
import SwiftUI

struct TakeoffConfigurationView: View {
Expand Down
Loading
Loading