Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,9 @@ jobs:
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
defaults write com.apple.iphonesimulator ConnectHardwareKeyboard -bool NO
# Enabled so Cmd+A works in clearAndType on iPad fields where iOS 26
# Liquid Glass reports the text field as not-hittable for triple-tap.
defaults write com.apple.iphonesimulator ConnectHardwareKeyboard -bool YES
xcrun simctl status_bar "$UDID" override \
--time "9:41" --dataNetwork wifi --wifiMode active --wifiBars 3 \
--cellularMode active --cellularBars 4 --batteryState charged --batteryLevel 100 || true
Expand Down
66 changes: 35 additions & 31 deletions SF50 TOLD/Loaders/TerrainLoader/TerrainDataLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ final class TerrainDataLoader: ObservableObject {
@Published private(set) var corruptedRegions: Set<TerrainRegion> = []

/// Logger for debug output.
private let logger = Logger(
nonisolated private let logger = Logger(
subsystem: "codes.tim.SF50-TOLD",
category: "TerrainDataLoader"
)

/// App group identifier for shared storage.
private let appGroupID = "group.codes.tim.TOLD"
nonisolated private let appGroupID = "group.codes.tim.TOLD"

/// Base URL for terrain data downloads.
private var baseURL = TerrainManifest.defaultBaseURL.absoluteString
Expand All @@ -82,7 +82,7 @@ final class TerrainDataLoader: ObservableObject {
}()

/// Returns the URL to the terrain directory in the app group container.
private var terrainDirectory: URL? {
nonisolated private var terrainDirectory: URL? {
guard
let containerURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: appGroupID
Expand Down Expand Up @@ -177,37 +177,41 @@ final class TerrainDataLoader: ObservableObject {
/// Refreshes the list of available regions by scanning the terrain directory.
///
/// Also detects compressed `.srtm.lzma` files left by the Background Assets
/// extension and triggers async decompression for them.
/// extension and triggers async decompression for them. The filesystem scan
/// runs on a background task so the main actor is never blocked.
func refreshAvailableRegions() {
var available = Set<TerrainRegion>()
var pendingDecompression = [TerrainRegion]()

for region in TerrainRegion.allCases {
if corruptedRegions.contains(region) { continue }

if let url = decompressedFileURL(for: region),
FileManager.default.fileExists(atPath: url.path)
{
available.insert(region)
} else if let compressed = compressedFileURL(for: region),
FileManager.default.fileExists(atPath: compressed.path)
{
logger.info(
"Found BA-downloaded compressed file for \(region.rawValue), queuing decompression"
)
pendingDecompression.append(region)
let corrupted = corruptedRegions
Task.detached(priority: .userInitiated) { [self] in
var available = Set<TerrainRegion>()
var pendingDecompression = [TerrainRegion]()

for region in TerrainRegion.allCases where !corrupted.contains(region) {
if let url = decompressedFileURL(for: region),
FileManager.default.fileExists(atPath: url.path)
{
available.insert(region)
} else if let compressed = compressedFileURL(for: region),
FileManager.default.fileExists(atPath: compressed.path)
{
logger.info(
"Found BA-downloaded compressed file for \(region.rawValue), queuing decompression"
)
pendingDecompression.append(region)
}
}
}

availableRegions = available
logger.info("Available terrain regions: \(available.map(\.rawValue))")
await MainActor.run {
self.availableRegions = available
self.logger.info("Available terrain regions: \(available.map(\.rawValue))")

loadAvailableRegionsIntoService()
self.loadAvailableRegionsIntoService()

// Decompress any files left by the Background Assets extension
for region in pendingDecompression {
Task {
await decompressPendingDownload(for: region)
// Decompress any files left by the Background Assets extension
for region in pendingDecompression {
Task {
await self.decompressPendingDownload(for: region)
}
}
}
}
}
Expand Down Expand Up @@ -339,12 +343,12 @@ final class TerrainDataLoader: ObservableObject {
// MARK: - Private Methods

/// Returns the URL to the decompressed terrain file for a region.
private func decompressedFileURL(for region: TerrainRegion) -> URL? {
nonisolated private func decompressedFileURL(for region: TerrainRegion) -> URL? {
terrainDirectory?.appendingPathComponent("\(region.rawValue).srtm")
}

/// Returns the URL to the compressed terrain file for a region.
private func compressedFileURL(for region: TerrainRegion) -> URL? {
nonisolated private func compressedFileURL(for region: TerrainRegion) -> URL? {
terrainDirectory?.appendingPathComponent(region.filename)
}

Expand Down
36 changes: 1 addition & 35 deletions SF50 TOLD/SF50_TOLDApp.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import Defaults
import SF50_Shared
import Sentry
import SwiftData
import SwiftNASR
import SwiftUI
import WidgetKit

Expand Down Expand Up @@ -89,21 +87,10 @@ struct SF50_TOLDApp: App {
}

init() {
// Handle UI testing mode
let isGeneratingScreenshots = ProcessInfo.processInfo.arguments.contains("GENERATE-SCREENSHOTS")
let isUITesting = ProcessInfo.processInfo.arguments.contains("UI-TESTING")

if isUITesting {
if ProcessInfo.processInfo.arguments.contains("UI-TESTING") {
UITestingHelper.setupUITestingEnvironment(container: sharedModelContainer)
}

// Purge stale navigation data before anything can access it
if !isGeneratingScreenshots && !isUITesting
&& Defaults[.schemaVersion] != latestSchemaVersion
{
Self.purgeStaleNavigationData(from: sharedModelContainer)
}

SentrySDK.start { options in
options.dsn =
"https://18ccb9d2342467fafcaebcc33cc676e5@o4510156629475328.ingest.us.sentry.io/4510161674502144"
Expand Down Expand Up @@ -141,25 +128,4 @@ struct SF50_TOLDApp: App {
}
}
}

private static func purgeStaleNavigationData(from container: ModelContainer) {
let context = container.mainContext
do {
try context.delete(model: Airport.self)
try context.delete(model: Runway.self)
try context.delete(model: Procedure.self)
try context.delete(model: ProcedureSegment.self)
try context.delete(model: Leg.self)
try context.delete(model: Navaid.self)
try context.delete(model: Obstacle.self)
try context.delete(model: NOTAM.self)
try context.delete(model: Cycle.self)
try context.save()
} catch {
SentrySDK.capture(error: error) { scope in
scope.setTag(value: "purge", key: "swiftData.operation")
scope.setFingerprint(["swiftData", "purge"])
}
}
}
}
11 changes: 9 additions & 2 deletions SF50 TOLDUITests/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,15 @@ extension XCUIElement {
if app.keyboards.firstMatch.waitForExistence(timeout: 2) { break }
}

// Select all existing text and type the replacement.
tap(withNumberOfTaps: 3, numberOfTouches: 1)
// Select all existing text. On iPhone, triple-tap works natively. On
// iPad with iOS 26 Liquid Glass overlays, the element can report "not
// hittable" and the XCUIElement triple-tap fails, so fall back to
// hardware-keyboard-emulated Cmd+A which bypasses hit-testing.
if isHittable {
tap(withNumberOfTaps: 3, numberOfTouches: 1)
} else {
typeKey("a", modifierFlags: .command)
}
Thread.sleep(forTimeInterval: 0.3)
typeText(text)
}
Expand Down
Loading