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
56 changes: 28 additions & 28 deletions SingleThread.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = SingleThread/SingleThread.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -784,7 +784,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThread;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand All @@ -810,7 +810,7 @@
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = SingleThread/SingleThread.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -835,7 +835,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThread;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand All @@ -856,12 +856,12 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThreadTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
Expand All @@ -885,12 +885,12 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThreadTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
Expand All @@ -913,12 +913,12 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThreadUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
Expand All @@ -937,12 +937,12 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThreadUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
Expand All @@ -962,14 +962,14 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = SingleThread;
INFOPLIST_KEY_NSRemindersFullAccessUsageDescription = "SingleThread needs access to show your reminders.";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = app.alanvardy.SingleThread;
INFOPLIST_KEY_WKWatchOnly = NO;
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThread.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
Expand All @@ -990,14 +990,14 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = SingleThread;
INFOPLIST_KEY_NSRemindersFullAccessUsageDescription = "SingleThread needs access to show your reminders.";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = app.alanvardy.SingleThread;
INFOPLIST_KEY_WKWatchOnly = NO;
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThread.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
Expand All @@ -1018,7 +1018,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = SingleThread/AppGroup.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SingleThreadWidget/Info.plist;
Expand All @@ -1030,7 +1030,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThread.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand All @@ -1049,7 +1049,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = SingleThread/AppGroup.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SingleThreadWidget/Info.plist;
Expand All @@ -1061,7 +1061,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThread.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand All @@ -1079,10 +1079,10 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThreadWatchUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
Expand All @@ -1101,10 +1101,10 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThreadWatchUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
Expand All @@ -1124,10 +1124,10 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThreadWatchTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
Expand All @@ -1148,10 +1148,10 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 6NWX2DHB9Q;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.3;
MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = app.alanvardy.SingleThreadWatchTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
Expand Down
3 changes: 2 additions & 1 deletion SingleThread/ContentView+Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ extension ContentView {
availableLists: viewModel.store.availableLists,
excludedLists: excludedListsBinding,
entitlementStore: viewModel.store.entitlementStore,
viewModel: SettingsViewModel())
viewModel: SettingsViewModel(),
initialRoute: LaunchArgument.isPresent(.openSettingsReminder) ? .reminder : nil)
// Standard-suite keys are plain in-memory values; write each change
// back to the @AppStorage-backed property so settings survive relaunch.
// App-Group keys write straight through their store types instead
Expand Down
11 changes: 6 additions & 5 deletions SingleThread/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,12 @@ struct ContentView: View {
handleScenePhaseChange(phase)
}
.task {
// `--ui-testing-open-settings` replays the gear button's action at
// launch. `simctl` cannot tap, so this is the only way screenshot
// automation can reach the options screen; bag first, then the flag,
// because `settingsSheetContent` renders nothing until the bag exists.
if LaunchArgument.isPresent(.openSettings), settingsBag == nil {
// The settings seams replay the gear button's action at launch.
// `simctl` cannot tap, so this is the only way screenshot automation
// reaches the options screen (and, for `openSettingsReminder`, its
// Reminder subscreen); bag first, then the flag, because
// `settingsSheetContent` renders nothing until the bag exists.
if LaunchArgument.opensSettings(), settingsBag == nil {
settingsBag = makeSettingsBag()
isShowingSettings = true
}
Expand Down
16 changes: 16 additions & 0 deletions SingleThread/LaunchArgument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,24 @@ enum LaunchArgument: String {
/// until the bag exists.
case openSettings = "--ui-testing-open-settings"

/// Presents the root Settings sheet and pushes the "Reminder" subscreen.
///
/// The subscreen is otherwise reachable only by tapping the Reminder row,
/// which `simctl` cannot do, so the App Store screenshot harness has no other
/// route to it. It opens the sheet exactly as ``openSettings`` does — same
/// bag-then-flag order — and then seeds the navigation path with the row's
/// destination.
case openSettingsReminder = "--ui-testing-open-settings-reminder"

// MARK: Internal

/// Whether either Settings seam is present — the sheet opener or the
/// Reminder-subscreen opener (which opens the sheet too).
static func opensSettings(
in arguments: [String] = ProcessInfo.processInfo.arguments) -> Bool {
isPresent(.openSettings, in: arguments) || isPresent(.openSettingsReminder, in: arguments)
}

/// Whether `argument` was passed to this process.
static func isPresent(
_ argument: Self,
Expand Down
58 changes: 46 additions & 12 deletions SingleThread/SettingsView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import SingleThreadCore
import SwiftUI

// MARK: - SettingsRoute

/// One pushed destination inside the Settings stack. Only the Reminder row is
/// value-driven: the screenshot harness seeds it through
/// ``LaunchArgument/openSettingsReminder``, and the value form is what lets a
/// `NavigationPath` pre-select it before the stack renders.
enum SettingsRoute: Hashable {
case reminder
}

// MARK: - SettingsView

/// Modal settings screen presented from the gear button. Owns no state —
Expand All @@ -22,20 +32,28 @@ struct SettingsView: View {
availableLists: [String],
excludedLists: Binding<Set<String>>,
entitlementStore: EntitlementStore = EntitlementStore(),
viewModel: SettingsViewModel = SettingsViewModel()) {
viewModel: SettingsViewModel = SettingsViewModel(),
initialRoute: SettingsRoute? = nil) {
self.bindings = bindings
self.viewModel = viewModel
self.store = store
self.backgroundImage = backgroundImage
self.availableLists = availableLists
self.entitlementStore = entitlementStore
_excludedLists = excludedLists
// Seed the path before the stack renders so a seam-driven launch lands
// on the subscreen in the first frame instead of pushing afterwards.
var path = NavigationPath()
if let initialRoute {
path.append(initialRoute)
}
_path = State(initialValue: path)
}

// MARK: Internal

var body: some View {
NavigationStack {
NavigationStack(path: $path) {
List {
Section {
NavigationLink {
Expand Down Expand Up @@ -80,16 +98,7 @@ struct SettingsView: View {
}
.accessibilityIdentifier("settingsNotificationsRow")
#endif
NavigationLink {
ReminderSettingsView(
showDate: $bindings.showDate,
showList: $bindings.showList,
showRecurrence: $bindings.showRecurrence,
showAlarms: $bindings.showAlarms,
showCompletionGlow: $bindings.showCompletionGlow,
showCompletionMomentum: $bindings.showCompletionMomentum,
viewModel: viewModel)
} label: {
NavigationLink(value: SettingsRoute.reminder) {
SettingsLinkLabel(
title: SharedStrings.reminder,
systemImage: "bell.badge",
Expand Down Expand Up @@ -177,6 +186,19 @@ struct SettingsView: View {
.accessibilityAddTraits(.isButton)
}
}
.navigationDestination(for: SettingsRoute.self) { route in
switch route {
case .reminder:
ReminderSettingsView(
showDate: $bindings.showDate,
showList: $bindings.showList,
showRecurrence: $bindings.showRecurrence,
showAlarms: $bindings.showAlarms,
showCompletionGlow: $bindings.showCompletionGlow,
showCompletionMomentum: $bindings.showCompletionMomentum,
viewModel: viewModel)
}
}
.localizedNavigationTitle("Settings")
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Expand All @@ -188,13 +210,25 @@ struct SettingsView: View {
}
}
.modifier(TextSizeModifier(textSize: bindings.textSize))
.onDisappear {
// Drop the seeded route when the sheet goes away so a re-presented
// Settings opens at its root. Reading `path` directly — not only
// through `$path` — is also what keeps it visible to Periphery,
// which misses `$`-projection-only `@State` under Xcode 27 and would
// otherwise report the property as unused.
if !path.isEmpty {
path = NavigationPath()
}
}
}

// MARK: Private

@Environment(\.dismiss)
private var dismiss

@State private var path: NavigationPath

@Binding private var excludedLists: Set<String>

@Bindable private var bindings: SettingsBindings
Expand Down
4 changes: 3 additions & 1 deletion SingleThreadTests/AISortCoordinatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,9 @@ struct AISortCoordinatorTests {
/// Polls until `condition` holds (or `timeout` elapses). Under parallel Swift
/// Testing on macOS the coordinator's debounced MainActor task can be delayed
/// past any fixed sleep, so the tests must await the *condition*, not a duration.
private func waitUntil(timeout: Duration = .seconds(20), _ condition: () -> Bool) async {
/// 25 s leaves headroom over the observed ~20.7 s worst case on a loaded host;
/// at the previous 20 s bound the poll expired and the caller's `#expect` failed.
private func waitUntil(timeout: Duration = .seconds(25), _ condition: () -> Bool) async {
let clock = ContinuousClock()
let deadline = clock.now + timeout
while !condition(), clock.now < deadline {
Expand Down
Loading