diff options
| author | Christian Cleberg <[email protected]> | 2026-08-07 17:33:46 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-08-07 17:33:46 -0500 |
| commit | 283fbf00fdffd64c924b6cb22c84e803b718f078 (patch) | |
| tree | f6603518ba62134faa699d864003b90464930ab9 | |
| parent | b2a1265cadccc34cbdab71b9ed638b908cac9f62 (diff) | |
| download | hutch-283fbf00fdffd64c924b6cb22c84e803b718f078.tar.gz hutch-283fbf00fdffd64c924b6cb22c84e803b718f078.tar.bz2 hutch-283fbf00fdffd64c924b6cb22c84e803b718f078.zip | |
Adopt Swift 6 language mode
Flip SWIFT_VERSION to 6.0 (keeping SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor)
and resolve every resulting diagnostic. Mechanical, no behaviour change:
- Models: pure value types in Models/ and utility extensions (Date, DateFormatter,
SRHTWebURL) marked nonisolated so the nonisolated networking layer can use them.
- App Intents: stored 'static var' (title/description/openAppWhenRun/
typeDisplayRepresentation/defaultQuery/caseDisplayRepresentations) -> 'static let';
drop the invalid @retroactive on the in-module LookupType: AppEnum.
- Sendable dicts: 'nil as String? as Any' -> 'nil as String? as any Sendable'.
- UserDefaults: retroactive @unchecked Sendable (documented thread-safe).
- WidgetKit completion handlers rebound nonisolated(unsafe) to cross into Task {};
TipStoreViewModel task handle is @ObservationIgnored nonisolated(unsafe) for
its nonisolated deinit.
- Tests run on @MainActor; a few constant fixtures marked nonisolated for use in
@Sendable stub closures.
Builds and the full suite are clean in Swift 6 mode. Roadmap updated.
71 files changed, 273 insertions, 209 deletions
diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj index dfa2b62..355157b 100644 --- a/Hutch.xcodeproj/project.pbxproj +++ b/Hutch.xcodeproj/project.pbxproj @@ -622,7 +622,7 @@ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -659,7 +659,7 @@ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -680,7 +680,7 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch"; }; @@ -702,7 +702,7 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch"; }; @@ -732,7 +732,7 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -761,7 +761,7 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -790,7 +790,7 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -819,7 +819,7 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/Hutch/App/HutchIntents.swift b/Hutch/App/HutchIntents.swift index ff8fec8..3f4cd83 100644 --- a/Hutch/App/HutchIntents.swift +++ b/Hutch/App/HutchIntents.swift @@ -4,9 +4,9 @@ import Foundation // MARK: - Navigation Intents struct OpenWorkQueueIntent: AppIntent { - static var title: LocalizedStringResource = "Open Work Queue" - static var description = IntentDescription("Opens Hutch to your Work Queue.") - static var openAppWhenRun = true + static let title: LocalizedStringResource = "Open Work Queue" + static let description = IntentDescription("Opens Hutch to your Work Queue.") + static let openAppWhenRun = true var route: HutchRoute { .workQueue(scope: .all) } @@ -18,9 +18,9 @@ struct OpenWorkQueueIntent: AppIntent { } struct OpenRecentActivityIntent: AppIntent { - static var title: LocalizedStringResource = "Open Recent Activity" - static var description = IntentDescription("Opens Hutch to recent activity.") - static var openAppWhenRun = true + static let title: LocalizedStringResource = "Open Recent Activity" + static let description = IntentDescription("Opens Hutch to recent activity.") + static let openAppWhenRun = true var route: HutchRoute { .recentActivity } @@ -32,9 +32,9 @@ struct OpenRecentActivityIntent: AppIntent { } struct OpenSystemStatusIntent: AppIntent { - static var title: LocalizedStringResource = "Open System Status" - static var description = IntentDescription("Opens Hutch to SourceHut system status.") - static var openAppWhenRun = true + static let title: LocalizedStringResource = "Open System Status" + static let description = IntentDescription("Opens Hutch to SourceHut system status.") + static let openAppWhenRun = true var route: HutchRoute { .systemStatus } @@ -46,9 +46,9 @@ struct OpenSystemStatusIntent: AppIntent { } struct OpenPinnedResourceIntent: AppIntent { - static var title: LocalizedStringResource = "Open Pinned Resource" - static var description = IntentDescription("Opens a pinned Hutch resource.") - static var openAppWhenRun = true + static let title: LocalizedStringResource = "Open Pinned Resource" + static let description = IntentDescription("Opens a pinned Hutch resource.") + static let openAppWhenRun = true @Parameter(title: "Pinned Resource") var pinnedResource: PinnedResourceEntity @@ -63,9 +63,9 @@ struct OpenPinnedResourceIntent: AppIntent { } struct OpenProjectDashboardIntent: AppIntent { - static var title: LocalizedStringResource = "Open Project Dashboard" - static var description = IntentDescription("Opens a pinned project dashboard in Hutch.") - static var openAppWhenRun = true + static let title: LocalizedStringResource = "Open Project Dashboard" + static let description = IntentDescription("Opens a pinned project dashboard in Hutch.") + static let openAppWhenRun = true @Parameter(title: "Project") var project: ProjectEntity @@ -84,16 +84,16 @@ struct OpenProjectDashboardIntent: AppIntent { enum HutchShortcutScope: String, AppEnum { case all - static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scope") - static var caseDisplayRepresentations: [HutchShortcutScope: DisplayRepresentation] = [ + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scope") + static let caseDisplayRepresentations: [HutchShortcutScope: DisplayRepresentation] = [ .all: "All" ] } struct OpenFailedBuildsIntent: AppIntent { - static var title: LocalizedStringResource = "Open Failed Builds" - static var description = IntentDescription("Opens Hutch to failed builds.") - static var openAppWhenRun = true + static let title: LocalizedStringResource = "Open Failed Builds" + static let description = IntentDescription("Opens Hutch to failed builds.") + static let openAppWhenRun = true @Parameter(title: "Scope", default: .all) var scope: HutchShortcutScope @@ -108,9 +108,9 @@ struct OpenFailedBuildsIntent: AppIntent { } struct OpenAssignedTicketsIntent: AppIntent { - static var title: LocalizedStringResource = "Open Assigned Tickets" - static var description = IntentDescription("Opens Hutch to tickets assigned to you.") - static var openAppWhenRun = true + static let title: LocalizedStringResource = "Open Assigned Tickets" + static let description = IntentDescription("Opens Hutch to tickets assigned to you.") + static let openAppWhenRun = true @Parameter(title: "Scope", default: .all) var scope: HutchShortcutScope @@ -125,9 +125,9 @@ struct OpenAssignedTicketsIntent: AppIntent { } struct SearchHutchIntent: AppIntent { - static var title: LocalizedStringResource = "Search Hutch" - static var description = IntentDescription("Opens Hutch lookup with a search query.") - static var openAppWhenRun = true + static let title: LocalizedStringResource = "Search Hutch" + static let description = IntentDescription("Opens Hutch lookup with a search query.") + static let openAppWhenRun = true @Parameter(title: "Query") var query: String @@ -155,8 +155,8 @@ struct SearchHutchIntent: AppIntent { // MARK: - App Entities struct PinnedResourceEntity: AppEntity, Identifiable { - static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Pinned Resource") - static var defaultQuery = PinnedResourceQuery() + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Pinned Resource") + static let defaultQuery = PinnedResourceQuery() let id: String let name: String @@ -181,8 +181,8 @@ struct PinnedResourceQuery: EntityQuery { } struct ProjectEntity: AppEntity, Identifiable { - static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project") - static var defaultQuery = ProjectEntityQuery() + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project") + static let defaultQuery = ProjectEntityQuery() let id: String let name: String @@ -278,8 +278,8 @@ enum HutchIntentEntityStore { // MARK: - Existing Read-Only Summary Intents struct CheckSystemStatusIntent: AppIntent { - static var title: LocalizedStringResource = "Check SourceHut Status" - static var description = IntentDescription("Returns the current SourceHut system status.") + static let title: LocalizedStringResource = "Check SourceHut Status" + static let description = IntentDescription("Returns the current SourceHut system status.") @MainActor func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog { @@ -302,8 +302,8 @@ struct CheckSystemStatusIntent: AppIntent { } struct CheckBuildsIntent: AppIntent { - static var title: LocalizedStringResource = "Check Hutch Builds" - static var description = IntentDescription("Returns a summary of your recent build status.") + static let title: LocalizedStringResource = "Check Hutch Builds" + static let description = IntentDescription("Returns a summary of your recent build status.") @MainActor func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog { @@ -357,8 +357,8 @@ extension LookupType: AppEnum { // MARK: - Mutating Intents struct ClearRecentActivityIntent: AppIntent { - static var title: LocalizedStringResource = "Clear Recent Activity" - static var description = IntentDescription("Clears the Recent list on the Hutch Home tab.") + static let title: LocalizedStringResource = "Clear Recent Activity" + static let description = IntentDescription("Clears the Recent list on the Hutch Home tab.") @MainActor func perform() async throws -> some IntentResult & ProvidesDialog { @@ -368,8 +368,8 @@ struct ClearRecentActivityIntent: AppIntent { } struct UnpinResourceIntent: AppIntent { - static var title: LocalizedStringResource = "Unpin Resource" - static var description = IntentDescription("Removes a pinned resource from the Hutch Home tab.") + static let title: LocalizedStringResource = "Unpin Resource" + static let description = IntentDescription("Removes a pinned resource from the Hutch Home tab.") @Parameter(title: "Pinned Resource") var pinnedResource: PinnedResourceEntity diff --git a/Hutch/Extensions/Date+Relative.swift b/Hutch/Extensions/Date+Relative.swift index f06ba59..b6c28ed 100644 --- a/Hutch/Extensions/Date+Relative.swift +++ b/Hutch/Extensions/Date+Relative.swift @@ -1,6 +1,6 @@ import Foundation -extension Date { +nonisolated extension Date { /// A short relative description like "2h ago", "3d ago", or "Jan 5, 2025". var relativeDescription: String { let formatter = RelativeDateTimeFormatter() diff --git a/Hutch/Extensions/DateFormatter+SRHT.swift b/Hutch/Extensions/DateFormatter+SRHT.swift index c017b11..a06b58a 100644 --- a/Hutch/Extensions/DateFormatter+SRHT.swift +++ b/Hutch/Extensions/DateFormatter+SRHT.swift @@ -1,6 +1,6 @@ import Foundation -extension DateFormatter { +nonisolated extension DateFormatter { /// Formatter for the sr.ht `Time` scalar: `%Y-%m-%dT%H:%M:%SZ` (UTC). static let srht: DateFormatter = { let formatter = DateFormatter() diff --git a/Hutch/Extensions/Foundation+Sendable.swift b/Hutch/Extensions/Foundation+Sendable.swift new file mode 100644 index 0000000..9a12aa6 --- /dev/null +++ b/Hutch/Extensions/Foundation+Sendable.swift @@ -0,0 +1,7 @@ +import Foundation + +// `UserDefaults` is documented as thread-safe but is not marked `Sendable` by +// Foundation. Hutch passes it into account sessions and the various per-account +// stores, all of which need to cross concurrency boundaries. The retroactive +// unchecked conformance reflects the documented thread-safety. +extension UserDefaults: @retroactive @unchecked Sendable {} diff --git a/Hutch/Extensions/SRHTWebURL.swift b/Hutch/Extensions/SRHTWebURL.swift index fc5a1d5..5715ce4 100644 --- a/Hutch/Extensions/SRHTWebURL.swift +++ b/Hutch/Extensions/SRHTWebURL.swift @@ -1,6 +1,6 @@ import Foundation -enum SRHTWebURL { +nonisolated enum SRHTWebURL { static let chat = URL(string: "https://chat.sr.ht")! static let status = URL(string: "https://status.sr.ht")! /// RSS/Atom feed for status incidents (HTML snapshot uses `status`). diff --git a/Hutch/Models/AccountEntry.swift b/Hutch/Models/AccountEntry.swift index 551cd19..cfc2f62 100644 --- a/Hutch/Models/AccountEntry.swift +++ b/Hutch/Models/AccountEntry.swift @@ -1,7 +1,7 @@ import Foundation /// A stored sr.ht account (token + resolved username). -struct AccountEntry: Codable, Identifiable, Equatable, Sendable { +nonisolated struct AccountEntry: Codable, Identifiable, Equatable, Sendable { let id: String var username: String var token: String diff --git a/Hutch/Models/ArtifactInfo.swift b/Hutch/Models/ArtifactInfo.swift index dbe9d96..6418ba8 100644 --- a/Hutch/Models/ArtifactInfo.swift +++ b/Hutch/Models/ArtifactInfo.swift @@ -1,7 +1,7 @@ import Foundation /// An artifact with its parent reference name, used in the artifacts tab. -struct ArtifactInfo: Codable, Sendable, Identifiable { +nonisolated struct ArtifactInfo: Codable, Sendable, Identifiable { let id: Int let filename: String let checksum: String @@ -9,13 +9,13 @@ struct ArtifactInfo: Codable, Sendable, Identifiable { let url: URL } -struct ArtifactPage: Codable, Sendable { +nonisolated struct ArtifactPage: Codable, Sendable { let results: [ArtifactInfo] let cursor: String? } /// A reference (tag) that has associated artifacts. -struct ReferenceWithArtifacts: Codable, Sendable, Identifiable { +nonisolated struct ReferenceWithArtifacts: Codable, Sendable, Identifiable { var id: String { name } let name: String let artifacts: [ArtifactInfo] diff --git a/Hutch/Models/Builds.swift b/Hutch/Models/Builds.swift index 3926888..48a9c91 100644 --- a/Hutch/Models/Builds.swift +++ b/Hutch/Models/Builds.swift @@ -3,7 +3,7 @@ import Foundation // MARK: - Enums /// Status of a build job. -enum JobStatus: String, Codable, Sendable { +nonisolated enum JobStatus: String, Codable, Sendable { case pending = "PENDING" case queued = "QUEUED" case running = "RUNNING" @@ -30,7 +30,7 @@ enum JobStatus: String, Codable, Sendable { } /// Status of a single build task within a job. -enum TaskStatus: String, Codable, Sendable { +nonisolated enum TaskStatus: String, Codable, Sendable { case pending = "PENDING" case running = "RUNNING" case success = "SUCCESS" @@ -41,7 +41,7 @@ enum TaskStatus: String, Codable, Sendable { // MARK: - Build Task /// A single task within a build job. -struct BuildTask: Codable, Sendable, Identifiable, Equatable { +nonisolated struct BuildTask: Codable, Sendable, Identifiable, Equatable { private(set) var ordinal: Int? let name: String let status: TaskStatus @@ -74,7 +74,7 @@ struct BuildTask: Codable, Sendable, Identifiable, Equatable { // MARK: - Job Summary (for list view) /// Lightweight job model matching the fields returned by the jobs list query. -struct JobSummary: Codable, Sendable, Identifiable, Hashable { +nonisolated struct JobSummary: Codable, Sendable, Identifiable, Hashable { let id: Int let created: Date let updated: Date @@ -103,7 +103,7 @@ struct JobSummary: Codable, Sendable, Identifiable, Hashable { } /// Minimal task info for the list query. -struct JobTaskSummary: Codable, Sendable, Hashable { +nonisolated struct JobTaskSummary: Codable, Sendable, Hashable { let name: String let status: TaskStatus } @@ -111,7 +111,7 @@ struct JobTaskSummary: Codable, Sendable, Hashable { // MARK: - Job Detail (for detail view) /// Full job model with all fields for the detail view. -struct JobDetail: Codable, Sendable, Equatable { +nonisolated struct JobDetail: Codable, Sendable, Equatable { let id: Int let created: Date let updated: Date @@ -128,7 +128,7 @@ struct JobDetail: Codable, Sendable, Equatable { } /// A downloadable artifact emitted by a build job. -struct BuildArtifact: Codable, Sendable, Identifiable, Equatable { +nonisolated struct BuildArtifact: Codable, Sendable, Identifiable, Equatable { let id: Int let created: Date let path: String @@ -145,14 +145,14 @@ struct BuildArtifact: Codable, Sendable, Identifiable, Equatable { } /// The log associated with a build job. -struct BuildLog: Codable, Sendable, Equatable { +nonisolated struct BuildLog: Codable, Sendable, Equatable { let fullURL: String } // MARK: - Job Group /// A group of related build jobs. -struct JobGroup: Codable, Sendable, Identifiable { +nonisolated struct JobGroup: Codable, Sendable, Identifiable { let id: Int let created: Date let note: String? diff --git a/Hutch/Models/CommitDetail.swift b/Hutch/Models/CommitDetail.swift index c95199d..962d2a4 100644 --- a/Hutch/Models/CommitDetail.swift +++ b/Hutch/Models/CommitDetail.swift @@ -1,7 +1,7 @@ import Foundation /// Full commit detail returned by the revparse_single query. -struct CommitDetail: Codable, Sendable, Identifiable { +nonisolated struct CommitDetail: Codable, Sendable, Identifiable { let id: String let shortId: String let author: CommitAuthor @@ -26,39 +26,39 @@ struct CommitDetail: Codable, Sendable, Identifiable { } } -struct CommitTrailer: Codable, Sendable, Identifiable { +nonisolated struct CommitTrailer: Codable, Sendable, Identifiable { var id: String { "\(name):\(value)" } let name: String let value: String } -struct ParentCommit: Codable, Sendable, Identifiable, Hashable { +nonisolated struct ParentCommit: Codable, Sendable, Identifiable, Hashable { let id: String let shortId: String let author: ParentAuthor } -struct ParentAuthor: Codable, Sendable, Hashable { +nonisolated struct ParentAuthor: Codable, Sendable, Hashable { let name: String } -struct CommitTree: Codable, Sendable { +nonisolated struct CommitTree: Codable, Sendable { let entries: CommitTreeEntries } -struct CommitTreeEntries: Codable, Sendable { +nonisolated struct CommitTreeEntries: Codable, Sendable { let results: [CommitTreeEntry] let cursor: String? } -struct CommitTreeEntry: Codable, Sendable, Identifiable { +nonisolated struct CommitTreeEntry: Codable, Sendable, Identifiable { let id: String let name: String let mode: Int let object: CommitTreeObject? } -struct CommitTreeObject: Codable, Sendable { +nonisolated struct CommitTreeObject: Codable, Sendable { let type: String? let id: String? let shortId: String? diff --git a/Hutch/Models/CommitSummary.swift b/Hutch/Models/CommitSummary.swift index 88c3e7d..99ed233 100644 --- a/Hutch/Models/CommitSummary.swift +++ b/Hutch/Models/CommitSummary.swift @@ -2,7 +2,7 @@ import Foundation /// Lightweight commit model for list views. Matches the subset of fields /// returned by the repository log query. -struct CommitSummary: Codable, Sendable, Identifiable, Hashable { +nonisolated struct CommitSummary: Codable, Sendable, Identifiable, Hashable { let id: String let shortId: String let author: CommitAuthor @@ -15,7 +15,7 @@ struct CommitSummary: Codable, Sendable, Identifiable, Hashable { } /// A compact author representation used in commit list responses. -struct CommitAuthor: Codable, Sendable, Hashable { +nonisolated struct CommitAuthor: Codable, Sendable, Hashable { let name: String let email: String? let time: Date diff --git a/Hutch/Models/ContributionCalendar.swift b/Hutch/Models/ContributionCalendar.swift index a429fa2..cf87ca7 100644 --- a/Hutch/Models/ContributionCalendar.swift +++ b/Hutch/Models/ContributionCalendar.swift @@ -1,6 +1,6 @@ import Foundation -struct ContributionCalendarResponse: Decodable, Sendable, Hashable { +nonisolated struct ContributionCalendarResponse: Decodable, Sendable, Hashable { let actor: String let from: Date let to: Date @@ -9,7 +9,7 @@ struct ContributionCalendarResponse: Decodable, Sendable, Hashable { let indexingState: ContributionIndexingState let days: [ContributionDay] - enum CodingKeys: String, CodingKey { + nonisolated enum CodingKeys: String, CodingKey { case actor case from case to @@ -59,14 +59,14 @@ struct ContributionCalendarResponse: Decodable, Sendable, Hashable { } } -struct ContributionDay: Decodable, Sendable, Hashable, Identifiable { +nonisolated struct ContributionDay: Decodable, Sendable, Hashable, Identifiable { var id: Date { date } let date: Date let count: Int let score: Double - enum CodingKeys: String, CodingKey { + nonisolated enum CodingKeys: String, CodingKey { case date case count case score @@ -90,7 +90,7 @@ struct ContributionDay: Decodable, Sendable, Hashable, Identifiable { } } -struct ContributionStatsResponse: Decodable, Sendable, Hashable { +nonisolated struct ContributionStatsResponse: Decodable, Sendable, Hashable { let actor: String let from: Date let to: Date @@ -103,7 +103,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable { let longestStreak: Int let currentStreak: Int - enum CodingKeys: String, CodingKey { + nonisolated enum CodingKeys: String, CodingKey { case actor case from case to @@ -117,7 +117,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable { case currentStreak = "current_streak" } - struct StatsWindow: Sendable, Hashable { + nonisolated struct StatsWindow: Sendable, Hashable { let actor: String let from: Date let to: Date @@ -126,7 +126,7 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable { let indexingState: ContributionIndexingState } - struct StatsTotals: Sendable, Hashable { + nonisolated struct StatsTotals: Sendable, Hashable { let totalEvents: Int let totalScore: Double let activeDays: Int @@ -181,13 +181,13 @@ struct ContributionStatsResponse: Decodable, Sendable, Hashable { } } -enum ContributionIndexingState: String, Codable, Sendable, Hashable { +nonisolated enum ContributionIndexingState: String, Codable, Sendable, Hashable { case pending case indexed case error } -enum ContributionIntensity: Int, Sendable, CaseIterable { +nonisolated enum ContributionIntensity: Int, Sendable, CaseIterable { case empty = 0 case level1 = 1 case level2 = 2 @@ -210,12 +210,12 @@ enum ContributionIntensity: Int, Sendable, CaseIterable { } } -struct ContributionWeek: Sendable, Hashable { +nonisolated struct ContributionWeek: Sendable, Hashable { let startDate: Date let days: [ContributionDay] } -enum ContributionCalendarLayout { +nonisolated enum ContributionCalendarLayout { static func weekColumns( from days: [ContributionDay], calendar: Calendar = .contributionCalendar @@ -241,7 +241,7 @@ enum ContributionCalendarLayout { } } -enum ContributionDateParser { +nonisolated enum ContributionDateParser { static func parse(_ rawValue: String) -> Date? { let parts = rawValue.split(separator: "-", omittingEmptySubsequences: false) guard @@ -348,7 +348,7 @@ enum ContributionDateParser { } } -extension Calendar { +nonisolated extension Calendar { static var contributionCalendar: Calendar { var calendar = Calendar(identifier: .gregorian) calendar.firstWeekday = 1 diff --git a/Hutch/Models/Git.swift b/Hutch/Models/Git.swift index 26a5e2d..ae74f19 100644 --- a/Hutch/Models/Git.swift +++ b/Hutch/Models/Git.swift @@ -3,14 +3,14 @@ import Foundation // MARK: - Enums /// Repository visibility level. -enum Visibility: String, Codable, Sendable { +nonisolated enum Visibility: String, Codable, Sendable { case publicVisibility = "PUBLIC" case unlisted = "UNLISTED" case privateVisibility = "PRIVATE" } /// Repository access mode. -enum AccessMode: String, Codable, Sendable, CaseIterable { +nonisolated enum AccessMode: String, Codable, Sendable, CaseIterable { case ro = "RO" case rw = "RW" } @@ -19,14 +19,14 @@ enum AccessMode: String, Codable, Sendable, CaseIterable { /// The `Entity` GraphQL interface from git.sr.ht. Represents the owner of a /// resource (typically a user). -struct Entity: Codable, Sendable, Hashable { +nonisolated struct Entity: Codable, Sendable, Hashable { let canonicalName: String } // MARK: - Repository /// A git repository from git.sr.ht. -struct Repository: Codable, Sendable, Identifiable { +nonisolated struct Repository: Codable, Sendable, Identifiable { let id: Int let created: Date let updated: Date @@ -37,7 +37,7 @@ struct Repository: Codable, Sendable, Identifiable { let accessMode: AccessMode let owner: Entity - enum CodingKeys: String, CodingKey { + nonisolated enum CodingKeys: String, CodingKey { case id, created, updated, name, description, visibility, readme case accessMode = "access" case owner @@ -47,7 +47,7 @@ struct Repository: Codable, Sendable, Identifiable { // MARK: - Signature /// A Git commit/tag signature (author or committer). -struct Signature: Codable, Sendable { +nonisolated struct Signature: Codable, Sendable { let name: String let email: String let time: Date @@ -56,7 +56,7 @@ struct Signature: Codable, Sendable { // MARK: - Trailer /// A Git commit trailer (e.g. "Signed-off-by", "Co-authored-by"). -struct Trailer: Codable, Sendable { +nonisolated struct Trailer: Codable, Sendable { let name: String let value: String } @@ -64,7 +64,7 @@ struct Trailer: Codable, Sendable { // MARK: - Commit /// A Git commit from git.sr.ht. -struct Commit: Codable, Sendable, Identifiable { +nonisolated struct Commit: Codable, Sendable, Identifiable { let id: String let shortId: String let author: Signature @@ -77,13 +77,13 @@ struct Commit: Codable, Sendable, Identifiable { // MARK: - Reference /// A Git reference (branch or tag name). -struct Reference: Codable, Sendable, Hashable { +nonisolated struct Reference: Codable, Sendable, Hashable { let name: String let target: String? } /// A Git reference enriched with the date of its tip commit or tag, for display in the Refs tab. -struct ReferenceDetail: Sendable, Hashable { +nonisolated struct ReferenceDetail: Sendable, Hashable { let name: String let target: String? let date: Date? @@ -92,7 +92,7 @@ struct ReferenceDetail: Sendable, Hashable { // MARK: - TreeEntry /// An entry in a Git tree (file or directory). -struct TreeEntry: Codable, Sendable, Identifiable { +nonisolated struct TreeEntry: Codable, Sendable, Identifiable { let id: String let name: String let mode: Int? @@ -104,41 +104,41 @@ struct TreeEntry: Codable, Sendable, Identifiable { /// Both TextBlob and BinaryBlob have type == "BLOB"; they are /// distinguished by the presence of the "text" key (TextBlob) vs /// the "content" key (BinaryBlob). -enum GitObject: Sendable { +nonisolated enum GitObject: Sendable { case tree(GitTree) case textBlob(GitTextBlob) case binaryBlob(GitBinaryBlob) case unknown } -struct GitTree: Codable, Sendable { +nonisolated struct GitTree: Codable, Sendable { let id: String? let shortId: String? let entries: GitTreeEntryPage? } -struct GitTextBlob: Codable, Sendable { +nonisolated struct GitTextBlob: Codable, Sendable { let id: String? let shortId: String? let text: String? let size: Int? } -struct GitBinaryBlob: Codable, Sendable { +nonisolated struct GitBinaryBlob: Codable, Sendable { let id: String? let shortId: String? let size: Int? let content: String? } -struct GitTreeEntryPage: Codable, Sendable { +nonisolated struct GitTreeEntryPage: Codable, Sendable { let results: [TreeEntry] let cursor: String? } // MARK: - GitObject Codable -extension GitObject: Codable { +nonisolated extension GitObject: Codable { private enum CodingKeys: String, CodingKey { case type, id, shortId, entries, text, size, content case typename = "__typename" @@ -211,7 +211,7 @@ extension GitObject: Codable { } /// Convenience helpers for checking object type. -extension GitObject { +nonisolated extension GitObject { var isTree: Bool { if case .tree = self { return true } return false @@ -230,7 +230,7 @@ extension GitObject { // MARK: - Tag /// An annotated Git tag from git.sr.ht. -struct Tag: Codable, Sendable, Identifiable { +nonisolated struct Tag: Codable, Sendable, Identifiable { let id: String let shortId: String let name: String @@ -241,7 +241,7 @@ struct Tag: Codable, Sendable, Identifiable { // MARK: - Artifact /// A release artifact attached to a Git tag. -struct Artifact: Codable, Sendable, Identifiable { +nonisolated struct Artifact: Codable, Sendable, Identifiable { let id: Int let created: Date let filename: String diff --git a/Hutch/Models/Inbox.swift b/Hutch/Models/Inbox.swift index c8e41bb..dfed719 100644 --- a/Hutch/Models/Inbox.swift +++ b/Hutch/Models/Inbox.swift @@ -1,6 +1,6 @@ import Foundation -struct InboxThreadSummary: Identifiable, Hashable, Sendable { +nonisolated struct InboxThreadSummary: Identifiable, Hashable, Sendable { let rootEmailID: Int let rootMessageID: String let threadRootEmailIDs: [Int] @@ -76,7 +76,7 @@ struct InboxThreadSummary: Identifiable, Hashable, Sendable { } } -struct InboxMessage: Identifiable, Hashable, Sendable { +nonisolated struct InboxMessage: Identifiable, Hashable, Sendable { let id: Int let author: Entity let date: Date @@ -89,12 +89,12 @@ struct InboxMessage: Identifiable, Hashable, Sendable { let rawMessageURL: URL? } -enum InboxMessageContentBlock: Hashable, Sendable { +nonisolated enum InboxMessageContentBlock: Hashable, Sendable { case plainText(String) case diff(String) } -struct InboxThreadDetail: Sendable { +nonisolated struct InboxThreadDetail: Sendable { let id: String let rootEmailID: Int let rootMessageID: String @@ -114,7 +114,7 @@ struct InboxThreadDetail: Sendable { } } -extension InboxThreadDetail { +nonisolated extension InboxThreadDetail { var replyRecipient: String { "\(listOwner.canonicalName)/\(listName)@lists.sr.ht" } @@ -144,7 +144,7 @@ extension InboxThreadDetail { } } -struct MailComposeDraft: Sendable { +nonisolated struct MailComposeDraft: Sendable { let recipients: [String] let ccRecipients: [String] let subject: String @@ -155,20 +155,20 @@ struct MailComposeDraft: Sendable { } } -extension MailComposeDraft: Identifiable {} +nonisolated extension MailComposeDraft: Identifiable {} -struct InboxMailingListReference: Decodable, Sendable, Hashable, Identifiable { +nonisolated struct InboxMailingListReference: Decodable, Sendable, Hashable, Identifiable { let id: Int let rid: String let name: String let owner: Entity } -struct InboxPatchPreview: Decodable, Sendable, Hashable { +nonisolated struct InboxPatchPreview: Decodable, Sendable, Hashable { let subject: String? } -enum InboxReadStateStore { +nonisolated enum InboxReadStateStore { private static let key = "InboxThreadLastViewed" private static let baselineKey = "InboxUnreadBaseline" diff --git a/Hutch/Models/Meta.swift b/Hutch/Models/Meta.swift index f52fc76..e0a09de 100644 --- a/Hutch/Models/Meta.swift +++ b/Hutch/Models/Meta.swift @@ -3,7 +3,7 @@ import Foundation // MARK: - User Profile (full) /// Extended user profile from meta.sr.ht with all fields from the `me` query. -struct UserProfile: Codable, Sendable { +nonisolated struct UserProfile: Codable, Sendable { let username: String let canonicalName: String let email: String @@ -20,7 +20,7 @@ struct UserProfile: Codable, Sendable { // MARK: - SSH Key -struct SSHKey: Codable, Sendable, Identifiable { +nonisolated struct SSHKey: Codable, Sendable, Identifiable { let id: Int let comment: String? let created: Date @@ -34,27 +34,27 @@ struct SSHKey: Codable, Sendable, Identifiable { } } -struct SSHKeyPage: Codable, Sendable { +nonisolated struct SSHKeyPage: Codable, Sendable { let results: [SSHKey] let cursor: String? } // MARK: - PGP Key -struct PGPKey: Codable, Sendable, Identifiable { +nonisolated struct PGPKey: Codable, Sendable, Identifiable { let id: Int let fingerprint: String let created: Date } -struct PGPKeyPage: Codable, Sendable { +nonisolated struct PGPKeyPage: Codable, Sendable { let results: [PGPKey] let cursor: String? } // MARK: - Subscription -struct Subscription: Codable, Sendable { +nonisolated struct Subscription: Codable, Sendable { let status: String? let autorenew: Bool? let interval: String? @@ -62,7 +62,7 @@ struct Subscription: Codable, Sendable { // MARK: - Personal Access Token -struct PersonalAccessToken: Codable, Sendable, Identifiable { +nonisolated struct PersonalAccessToken: Codable, Sendable, Identifiable { let id: Int let issued: Date let expires: Date? @@ -72,7 +72,7 @@ struct PersonalAccessToken: Codable, Sendable, Identifiable { /// One entry in meta.sr.ht's audit log: a security-relevant action on the /// account, with the address it came from. -struct AuditLogEntry: Codable, Sendable, Identifiable { +nonisolated struct AuditLogEntry: Codable, Sendable, Identifiable { let id: Int let created: Date let ipAddress: String diff --git a/Hutch/Models/Paste.swift b/Hutch/Models/Paste.swift index b4c8f18..a8bf926 100644 --- a/Hutch/Models/Paste.swift +++ b/Hutch/Models/Paste.swift @@ -1,6 +1,6 @@ import Foundation -struct PasteFile: Codable, Sendable, Hashable, Identifiable { +nonisolated struct PasteFile: Codable, Sendable, Hashable, Identifiable { let filename: String? let hash: String let contents: URL? @@ -8,7 +8,7 @@ struct PasteFile: Codable, Sendable, Hashable, Identifiable { var id: String { hash } } -struct Paste: Codable, Sendable, Identifiable, Hashable { +nonisolated struct Paste: Codable, Sendable, Identifiable, Hashable { let id: String let created: Date let visibility: Visibility @@ -16,7 +16,7 @@ struct Paste: Codable, Sendable, Identifiable, Hashable { let user: Entity } -struct PasteUploadDraft: Identifiable, Equatable, Sendable { +nonisolated struct PasteUploadDraft: Identifiable, Equatable, Sendable { let id: UUID var filename: String var contents: String diff --git a/Hutch/Models/Patchset.swift b/Hutch/Models/Patchset.swift index 330bbbc..cc10b35 100644 --- a/Hutch/Models/Patchset.swift +++ b/Hutch/Models/Patchset.swift @@ -1,7 +1,7 @@ import Foundation /// Review state of a patchset on lists.sr.ht. -enum PatchsetStatus: String, Codable, Sendable, CaseIterable { +nonisolated enum PatchsetStatus: String, Codable, Sendable, CaseIterable { case unknown = "UNKNOWN" case proposed = "PROPOSED" case needsRevision = "NEEDS_REVISION" @@ -55,7 +55,7 @@ enum PatchsetStatus: String, Codable, Sendable, CaseIterable { /// A patchset as it appears in a mailing list listing, derived from the thread's /// root email rather than a dedicated patchsets query — `MailingList` exposes no /// such field. -struct PatchsetSummary: Identifiable, Hashable, Sendable { +nonisolated struct PatchsetSummary: Identifiable, Hashable, Sendable { let id: Int let subject: String let version: Int @@ -70,7 +70,7 @@ struct PatchsetSummary: Identifiable, Hashable, Sendable { } /// One email within a patchset: either the cover letter or a single patch. -struct PatchsetEmail: Identifiable, Hashable, Sendable { +nonisolated struct PatchsetEmail: Identifiable, Hashable, Sendable { let id: Int let subject: String let date: Date? @@ -88,13 +88,13 @@ struct PatchsetEmail: Identifiable, Hashable, Sendable { } /// A build or check reported against a patchset. -struct PatchsetToolResult: Identifiable, Hashable, Sendable { +nonisolated struct PatchsetToolResult: Identifiable, Hashable, Sendable { let id: Int let icon: PatchsetToolIcon let details: String } -enum PatchsetToolIcon: String, Codable, Sendable { +nonisolated enum PatchsetToolIcon: String, Codable, Sendable { case pending = "PENDING" case waiting = "WAITING" case success = "SUCCESS" @@ -112,7 +112,7 @@ enum PatchsetToolIcon: String, Codable, Sendable { } /// A patchset with its cover letter, patches, and review context. -struct PatchsetDetail: Sendable { +nonisolated struct PatchsetDetail: Sendable { let id: Int let created: Date let updated: Date diff --git a/Hutch/Models/Project.swift b/Hutch/Models/Project.swift index 8f5aa68..bd1a688 100644 --- a/Hutch/Models/Project.swift +++ b/Hutch/Models/Project.swift @@ -1,7 +1,7 @@ import Foundation -struct Project: Identifiable, Hashable, Sendable { - struct MailingList: Identifiable, Hashable, Sendable { +nonisolated struct Project: Identifiable, Hashable, Sendable { + nonisolated struct MailingList: Identifiable, Hashable, Sendable { let id: String let name: String let description: String? @@ -22,8 +22,8 @@ struct Project: Identifiable, Hashable, Sendable { } } - struct SourceRepo: Identifiable, Hashable, Sendable { - enum RepoType: String, Decodable, Sendable { + nonisolated struct SourceRepo: Identifiable, Hashable, Sendable { + nonisolated enum RepoType: String, Decodable, Sendable { case git = "GIT" case hg = "HG" @@ -47,7 +47,7 @@ struct Project: Identifiable, Hashable, Sendable { } } - struct Tracker: Identifiable, Hashable, Sendable { + nonisolated struct Tracker: Identifiable, Hashable, Sendable { let id: String let name: String let description: String? @@ -72,7 +72,7 @@ struct Project: Identifiable, Hashable, Sendable { let isFullyLoaded: Bool /// Identity and display fields for a project. - struct Metadata: Sendable, Hashable { + nonisolated struct Metadata: Sendable, Hashable { let id: String let name: String let description: String? @@ -82,7 +82,7 @@ struct Project: Identifiable, Hashable, Sendable { let updated: Date } - struct Resources: Sendable, Hashable { + nonisolated struct Resources: Sendable, Hashable { let mailingLists: [MailingList] let sources: [SourceRepo] let trackers: [Tracker] @@ -171,7 +171,7 @@ struct Project: Identifiable, Hashable, Sendable { } } -extension Project.MailingList { +nonisolated extension Project.MailingList { var displayName: String { Project.normalizedText(name) ?? "Untitled Mailing List" } @@ -185,7 +185,7 @@ extension Project.MailingList { } } -extension Project.SourceRepo { +nonisolated extension Project.SourceRepo { var displayName: String { Project.normalizedText(name) ?? "Untitled Repository" } @@ -203,7 +203,7 @@ extension Project.SourceRepo { } } -extension Project.Tracker { +nonisolated extension Project.Tracker { var displayName: String { Project.normalizedText(name) ?? "Untitled Tracker" } @@ -221,13 +221,13 @@ extension Project.Tracker { } } -extension String { +nonisolated extension String { var srhtUsername: String { hasPrefix("~") ? String(dropFirst()) : self } } -extension Visibility { +nonisolated extension Visibility { var displayName: String { switch self { case .publicVisibility: diff --git a/Hutch/Models/RepositoryACL.swift b/Hutch/Models/RepositoryACL.swift index e5f5eb1..e5d93f6 100644 --- a/Hutch/Models/RepositoryACL.swift +++ b/Hutch/Models/RepositoryACL.swift @@ -1,12 +1,12 @@ import Foundation -struct RepositoryACLEntry: Codable, Sendable, Identifiable, Hashable { +nonisolated struct RepositoryACLEntry: Codable, Sendable, Identifiable, Hashable { let id: Int let mode: AccessMode let entity: Entity } -extension AccessMode { +nonisolated extension AccessMode { var shortLabel: String { rawValue } var displayName: String { diff --git a/Hutch/Models/RepositorySummary.swift b/Hutch/Models/RepositorySummary.swift index b41589b..40ec7a4 100644 --- a/Hutch/Models/RepositorySummary.swift +++ b/Hutch/Models/RepositorySummary.swift @@ -3,7 +3,7 @@ import Foundation /// Lightweight repository model matching the fields returned by the /// repositories list query. Avoids optionalizing all fields on the full /// `Repository` model. -struct RepositorySummary: Codable, Sendable, Identifiable, Hashable { +nonisolated struct RepositorySummary: Codable, Sendable, Identifiable, Hashable { let id: Int /// GraphQL resource identifier used by `repository(rid:)` queries. let rid: String @@ -15,13 +15,13 @@ struct RepositorySummary: Codable, Sendable, Identifiable, Hashable { let owner: Entity let head: Reference? - enum CodingKeys: String, CodingKey { + nonisolated enum CodingKeys: String, CodingKey { case id, rid, service, name, description, visibility, updated, owner case head = "HEAD" } /// Grouped initializer fields (single parameter keeps APIs explicit without exceeding parameter-count limits). - struct Fields: Sendable, Hashable { + nonisolated struct Fields: Sendable, Hashable { let id: Int let rid: String let service: SRHTService @@ -59,7 +59,7 @@ struct RepositorySummary: Codable, Sendable, Identifiable, Hashable { } } -extension RepositorySummary { +nonisolated extension RepositorySummary { var defaultBranchName: String? { head?.name.replacingOccurrences(of: "refs/heads/", with: "") } diff --git a/Hutch/Models/SystemStatusModels.swift b/Hutch/Models/SystemStatusModels.swift index 9a664b0..ddd659c 100644 --- a/Hutch/Models/SystemStatusModels.swift +++ b/Hutch/Models/SystemStatusModels.swift @@ -1,6 +1,6 @@ import Foundation -enum StatusLevel: String, Codable, Sendable { +nonisolated enum StatusLevel: String, Codable, Sendable { case operational case degraded case majorOutage @@ -32,7 +32,7 @@ enum StatusLevel: String, Codable, Sendable { } } -struct StatusServiceState: Identifiable, Hashable, Codable, Sendable { +nonisolated struct StatusServiceState: Identifiable, Hashable, Codable, Sendable { let id: String let name: String let slug: String? @@ -40,7 +40,7 @@ struct StatusServiceState: Identifiable, Hashable, Codable, Sendable { let description: String? } -struct StatusIncident: Identifiable, Hashable, Codable, Sendable { +nonisolated struct StatusIncident: Identifiable, Hashable, Codable, Sendable { let id: String let title: String let summary: String? @@ -50,7 +50,7 @@ struct StatusIncident: Identifiable, Hashable, Codable, Sendable { let isActive: Bool? } -struct SystemStatusSnapshot: Hashable, Codable, Sendable { +nonisolated struct SystemStatusSnapshot: Hashable, Codable, Sendable { let services: [StatusServiceState] let activeIncidents: [StatusIncident] let lastUpdated: Date @@ -78,7 +78,7 @@ struct SystemStatusSnapshot: Hashable, Codable, Sendable { } } -struct SystemStatusPageData: Sendable { +nonisolated struct SystemStatusPageData: Sendable { let snapshot: SystemStatusSnapshot let recentIncidents: [StatusIncident] } diff --git a/Hutch/Models/TicketBulkAction.swift b/Hutch/Models/TicketBulkAction.swift index dcf7676..ea9cc01 100644 --- a/Hutch/Models/TicketBulkAction.swift +++ b/Hutch/Models/TicketBulkAction.swift @@ -1,6 +1,6 @@ import Foundation -enum TicketBulkActionKind: String, Sendable { +nonisolated enum TicketBulkActionKind: String, Sendable { case close case assign @@ -23,7 +23,7 @@ enum TicketBulkActionKind: String, Sendable { } } -struct TicketBulkActionResult: Identifiable, Sendable { +nonisolated struct TicketBulkActionResult: Identifiable, Sendable { let id = UUID() let action: TicketBulkActionKind let totalCount: Int @@ -82,7 +82,7 @@ struct TicketBulkActionResult: Identifiable, Sendable { } } -struct TicketBulkActionFailure: Sendable { +nonisolated struct TicketBulkActionFailure: Sendable { let ticketID: Int let message: String } diff --git a/Hutch/Models/Todo.swift b/Hutch/Models/Todo.swift index 129d4bf..d2ae889 100644 --- a/Hutch/Models/Todo.swift +++ b/Hutch/Models/Todo.swift @@ -3,7 +3,7 @@ import Foundation // MARK: - Enums /// Status of a ticket. -enum TicketStatus: String, Codable, Sendable, CaseIterable { +nonisolated enum TicketStatus: String, Codable, Sendable, CaseIterable { case reported = "REPORTED" case confirmed = "CONFIRMED" case inProgress = "IN_PROGRESS" @@ -30,7 +30,7 @@ enum TicketStatus: String, Codable, Sendable, CaseIterable { } /// Resolution of a ticket. -enum TicketResolution: String, Codable, Sendable { +nonisolated enum TicketResolution: String, Codable, Sendable { case unresolved = "UNRESOLVED" case fixed = "FIXED" case implemented = "IMPLEMENTED" @@ -64,7 +64,7 @@ enum TicketResolution: String, Codable, Sendable { } /// Authenticity of a ticket or comment. -enum Authenticity: String, Codable, Sendable { +nonisolated enum Authenticity: String, Codable, Sendable { case authentic = "AUTHENTIC" case tampered = "TAMPERED" case unauthenticated = "UNAUTHENTICATED" @@ -74,7 +74,7 @@ enum Authenticity: String, Codable, Sendable { /// A label that can be applied to tickets. /// Named `TicketLabel` to avoid collision with `SwiftUI.Label`. -struct TicketLabel: Codable, Sendable, Identifiable, Hashable { +nonisolated struct TicketLabel: Codable, Sendable, Identifiable, Hashable { let id: Int let name: String let backgroundColor: String @@ -83,7 +83,7 @@ struct TicketLabel: Codable, Sendable, Identifiable, Hashable { // MARK: - Tracker ACL -struct TrackerACLPermissions: Codable, Sendable, Hashable { +nonisolated struct TrackerACLPermissions: Codable, Sendable, Hashable { let browse: Bool let submit: Bool let comment: Bool @@ -91,7 +91,7 @@ struct TrackerACLPermissions: Codable, Sendable, Hashable { let triage: Bool } -struct TrackerACL: Codable, Sendable, Identifiable, Hashable { +nonisolated struct TrackerACL: Codable, Sendable, Identifiable, Hashable { let id: Int let created: Date let entity: Entity @@ -112,7 +112,7 @@ struct TrackerACL: Codable, Sendable, Identifiable, Hashable { } } -struct DefaultTrackerACL: Codable, Sendable, Hashable { +nonisolated struct DefaultTrackerACL: Codable, Sendable, Hashable { let browse: Bool let submit: Bool let comment: Bool @@ -133,7 +133,7 @@ struct DefaultTrackerACL: Codable, Sendable, Hashable { // MARK: - Tracker /// A bug tracker from todo.sr.ht. -struct Tracker: Codable, Sendable, Identifiable, Hashable { +nonisolated struct Tracker: Codable, Sendable, Identifiable, Hashable { let id: Int let created: Date let updated: Date @@ -154,7 +154,7 @@ struct Tracker: Codable, Sendable, Identifiable, Hashable { // MARK: - Tracker Summary (for list view) /// Lightweight tracker model matching the fields returned by the trackers list query. -struct TrackerSummary: Codable, Sendable, Identifiable, Hashable { +nonisolated struct TrackerSummary: Codable, Sendable, Identifiable, Hashable { let id: Int /// GraphQL resource identifier used by `tracker(id:)` queries. let rid: String @@ -168,7 +168,7 @@ struct TrackerSummary: Codable, Sendable, Identifiable, Hashable { // MARK: - Ticket Summary (for list view) /// Lightweight ticket model for the list query. -struct TicketSummary: Codable, Sendable, Identifiable, Hashable { +nonisolated struct TicketSummary: Codable, Sendable, Identifiable, Hashable { let id: Int let title: String let status: TicketStatus @@ -190,7 +190,7 @@ struct TicketSummary: Codable, Sendable, Identifiable, Hashable { // MARK: - Ticket Detail /// Full ticket model for the detail view. -struct TicketDetail: Codable, Sendable { +nonisolated struct TicketDetail: Codable, Sendable { let id: Int let created: Date let updated: Date @@ -207,7 +207,7 @@ struct TicketDetail: Codable, Sendable { // MARK: - Event /// A timeline event on a ticket. Each event contains one or more changes. -struct TicketEvent: Codable, Sendable, Identifiable { +nonisolated struct TicketEvent: Codable, Sendable, Identifiable { let id: Int let created: Date var changes: [EventChange] @@ -215,7 +215,7 @@ struct TicketEvent: Codable, Sendable, Identifiable { /// A single change within an event, decoded from the polymorphic EventDetail /// interface using inline fragments. -struct EventChange: Codable, Sendable, Identifiable { +nonisolated struct EventChange: Codable, Sendable, Identifiable { let id: UUID let eventType: String @@ -272,12 +272,12 @@ struct EventChange: Codable, Sendable, Identifiable { /// Decoded from the `mentioned` field which may be a Ticket (with `id`) or /// an Entity (with `canonicalName`) depending on the event type. -struct MentionTarget: Codable, Sendable { +nonisolated struct MentionTarget: Codable, Sendable { let id: Int? let canonicalName: String? } /// Label info as returned within a LabelUpdate event change. -struct EventLabel: Codable, Sendable { +nonisolated struct EventLabel: Codable, Sendable { let name: String } diff --git a/Hutch/Models/User.swift b/Hutch/Models/User.swift index 4ab7315..10576ad 100644 --- a/Hutch/Models/User.swift +++ b/Hutch/Models/User.swift @@ -1,7 +1,7 @@ import Foundation /// A Sourcehut user from meta.sr.ht. -struct User: Decodable, Sendable, Hashable { +nonisolated struct User: Decodable, Sendable, Hashable { let id: Int let created: String? let updated: String? diff --git a/Hutch/Views/Lists/MailingListListView.swift b/Hutch/Views/Lists/MailingListListView.swift index 1b159bf..b6ed2ff 100644 --- a/Hutch/Views/Lists/MailingListListView.swift +++ b/Hutch/Views/Lists/MailingListListView.swift @@ -100,7 +100,7 @@ final class MailingListListViewModel { query: Self.createMailingListMutation, variables: [ "name": trimmedName, - "description": trimmedDescription.isEmpty ? nil as String? as Any : trimmedDescription, + "description": trimmedDescription.isEmpty ? nil as String? as any Sendable : trimmedDescription, "visibility": visibility.rawValue ], responseType: Response.self diff --git a/Hutch/Views/More/TipStoreViewModel.swift b/Hutch/Views/More/TipStoreViewModel.swift index bc909c1..043b218 100644 --- a/Hutch/Views/More/TipStoreViewModel.swift +++ b/Hutch/Views/More/TipStoreViewModel.swift @@ -32,7 +32,9 @@ final class TipStoreViewModel { var errorMessage: String? var statusMessage: String? - private var transactionUpdatesTask: Task<Void, Never>? + // Internal task handle, not observable state; assigned only on the main + // actor and read once from the nonisolated deinit. + @ObservationIgnored nonisolated(unsafe) private var transactionUpdatesTask: Task<Void, Never>? init() { transactionUpdatesTask = Task.detached(priority: .background) { diff --git a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift index 82e1592..62a8b12 100644 --- a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift +++ b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift @@ -557,7 +557,7 @@ final class RepositoryDetailViewModel { variables: [ "repoId": repository.id, "revspec": revspec, - "file": nil as String? as Any + "file": nil as String? as any Sendable ], file: MultipartUploadFile( variablePath: "file", diff --git a/Hutch/Views/Repositories/SyntaxHighlighter.swift b/Hutch/Views/Repositories/SyntaxHighlighter.swift index f8b2cbb..dfab443 100644 --- a/Hutch/Views/Repositories/SyntaxHighlighter.swift +++ b/Hutch/Views/Repositories/SyntaxHighlighter.swift @@ -208,7 +208,7 @@ nonisolated final class SyntaxHighlighter { ] } -private extension UIColor { +nonisolated private extension UIColor { /// `#rrggbb` for HTML inline styles, or `nil` if the color isn't RGB-convertible. var hexRGBString: String? { var red: CGFloat = 0 diff --git a/Hutch/Views/Settings/SettingsViewModel.swift b/Hutch/Views/Settings/SettingsViewModel.swift index 8536e60..2d57ba5 100644 --- a/Hutch/Views/Settings/SettingsViewModel.swift +++ b/Hutch/Views/Settings/SettingsViewModel.swift @@ -198,9 +198,9 @@ final class SettingsViewModel { do { let input: [String: any Sendable] = [ "email": email, - "url": url.isEmpty ? nil as String? as Any : url, - "location": location.isEmpty ? nil as String? as Any : location, - "bio": bio.isEmpty ? nil as String? as Any : bio + "url": url.isEmpty ? nil as String? as any Sendable : url, + "location": location.isEmpty ? nil as String? as any Sendable : location, + "bio": bio.isEmpty ? nil as String? as any Sendable : bio ] let result = try await client.execute( service: .meta, @@ -243,7 +243,7 @@ final class SettingsViewModel { do { // The input variable has avatar set to null; the actual file // is sent as a separate multipart part per graphql-multipart-request-spec. - let input: [String: any Sendable] = ["avatar": nil as String? as Any] + let input: [String: any Sendable] = ["avatar": nil as String? as any Sendable] let result = try await client.executeMultipart( service: .meta, query: Self.updateUserMutation, @@ -286,7 +286,7 @@ final class SettingsViewModel { error = nil do { - let input: [String: any Sendable] = ["avatar": nil as String? as Any] + let input: [String: any Sendable] = ["avatar": nil as String? as any Sendable] let result = try await client.execute( service: .meta, query: Self.updateUserMutation, diff --git a/HutchTests/APICacheTests.swift b/HutchTests/APICacheTests.swift index 24db870..9a93783 100644 --- a/HutchTests/APICacheTests.swift +++ b/HutchTests/APICacheTests.swift @@ -3,6 +3,7 @@ import Testing @testable import Hutch @Suite(.serialized) +@MainActor struct APICacheTests { private struct Payload: Codable, Sendable, Equatable { let value: String diff --git a/HutchTests/AppStateTests.swift b/HutchTests/AppStateTests.swift index b7e1755..49baa17 100644 --- a/HutchTests/AppStateTests.swift +++ b/HutchTests/AppStateTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct AppStateTests { @Test diff --git a/HutchTests/BuildListViewModelTests.swift b/HutchTests/BuildListViewModelTests.swift index 8ee5b81..9b64621 100644 --- a/HutchTests/BuildListViewModelTests.swift +++ b/HutchTests/BuildListViewModelTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct BuildListViewModelTests { @Test diff --git a/HutchTests/BuildTaskLogSearchTests.swift b/HutchTests/BuildTaskLogSearchTests.swift index 4cb9984..8c7ddc2 100644 --- a/HutchTests/BuildTaskLogSearchTests.swift +++ b/HutchTests/BuildTaskLogSearchTests.swift @@ -1,6 +1,7 @@ import Testing @testable import Hutch +@MainActor struct BuildTaskLogSearchTests { @Test diff --git a/HutchTests/BuildTaskTests.swift b/HutchTests/BuildTaskTests.swift index 2e2a1e0..c7baae7 100644 --- a/HutchTests/BuildTaskTests.swift +++ b/HutchTests/BuildTaskTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct BuildTaskTests { @Test diff --git a/HutchTests/BundleUserAgentTests.swift b/HutchTests/BundleUserAgentTests.swift index c1a20b8..a4bda55 100644 --- a/HutchTests/BundleUserAgentTests.swift +++ b/HutchTests/BundleUserAgentTests.swift @@ -40,6 +40,7 @@ private final class CapturingURLProtocol: URLProtocol, @unchecked Sendable { /// Tests are serialized because CapturingURLProtocol uses shared static state. @Suite(.serialized) +@MainActor struct BundleUserAgentTests { // MARK: Bundle extension diff --git a/HutchTests/ContributionCalendarTests.swift b/HutchTests/ContributionCalendarTests.swift index 0328ad5..e5d625a 100644 --- a/HutchTests/ContributionCalendarTests.swift +++ b/HutchTests/ContributionCalendarTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct ContributionCalendarTests { @Test diff --git a/HutchTests/DeepLinkTests.swift b/HutchTests/DeepLinkTests.swift index cdc5bab..dc4f605 100644 --- a/HutchTests/DeepLinkTests.swift +++ b/HutchTests/DeepLinkTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct DeepLinkTests { @Test diff --git a/HutchTests/GitObjectTests.swift b/HutchTests/GitObjectTests.swift index 547c6f3..3546d11 100644 --- a/HutchTests/GitObjectTests.swift +++ b/HutchTests/GitObjectTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct GitObjectTests { @Test diff --git a/HutchTests/HomeViewModelTests.swift b/HutchTests/HomeViewModelTests.swift index 851b520..a050931 100644 --- a/HutchTests/HomeViewModelTests.swift +++ b/HutchTests/HomeViewModelTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct HomeViewModelTests { @Test diff --git a/HutchTests/HutchTests.swift b/HutchTests/HutchTests.swift index c681efa..98c68d3 100644 --- a/HutchTests/HutchTests.swift +++ b/HutchTests/HutchTests.swift @@ -7,6 +7,7 @@ import Testing +@MainActor struct HutchTests { @Test func example() async throws { diff --git a/HutchTests/InboxViewModelTests.swift b/HutchTests/InboxViewModelTests.swift index 5780b6a..9c35001 100644 --- a/HutchTests/InboxViewModelTests.swift +++ b/HutchTests/InboxViewModelTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct InboxViewModelTests { @Test diff --git a/HutchTests/LookupHistoryStoreTests.swift b/HutchTests/LookupHistoryStoreTests.swift index 414d016..af7c69b 100644 --- a/HutchTests/LookupHistoryStoreTests.swift +++ b/HutchTests/LookupHistoryStoreTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct LookupHistoryStoreTests { @Test diff --git a/HutchTests/MailingListActivityTests.swift b/HutchTests/MailingListActivityTests.swift index 46208cb..61a7e77 100644 --- a/HutchTests/MailingListActivityTests.swift +++ b/HutchTests/MailingListActivityTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct MailingListActivityTests { @Test diff --git a/HutchTests/ManPageCatalogTests.swift b/HutchTests/ManPageCatalogTests.swift index df995f8..e6bd2fc 100644 --- a/HutchTests/ManPageCatalogTests.swift +++ b/HutchTests/ManPageCatalogTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct ManPageCatalogTests { @Test func loadsBundledCatalog() { diff --git a/HutchTests/PatchsetTests.swift b/HutchTests/PatchsetTests.swift index 945131f..e181f14 100644 --- a/HutchTests/PatchsetTests.swift +++ b/HutchTests/PatchsetTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct PatchsetStatusTests { @Test @@ -46,6 +47,7 @@ struct PatchsetStatusTests { } } +@MainActor struct PatchsetSummaryTests { @Test diff --git a/HutchTests/ProjectFormTests.swift b/HutchTests/ProjectFormTests.swift index a4b4dfa..3e1fffc 100644 --- a/HutchTests/ProjectFormTests.swift +++ b/HutchTests/ProjectFormTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct ProjectFormTests { @Test func parsesTagsSplittingAndTrimming() { diff --git a/HutchTests/ProjectPinStoreTests.swift b/HutchTests/ProjectPinStoreTests.swift index 13df539..c040868 100644 --- a/HutchTests/ProjectPinStoreTests.swift +++ b/HutchTests/ProjectPinStoreTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct ProjectPinStoreTests { @Test func storesPinnedProjectsPerUser() { diff --git a/HutchTests/ProjectTests.swift b/HutchTests/ProjectTests.swift index 23faa0d..785c98f 100644 --- a/HutchTests/ProjectTests.swift +++ b/HutchTests/ProjectTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct ProjectTests { private enum Fixture { static let exampleWebsite = "https://example.com" diff --git a/HutchTests/ReadmeViewTests.swift b/HutchTests/ReadmeViewTests.swift index 9e053b8..96eb8c0 100644 --- a/HutchTests/ReadmeViewTests.swift +++ b/HutchTests/ReadmeViewTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct ReadmeViewTests { @Test @@ -28,6 +29,7 @@ struct ReadmeViewTests { } } +@MainActor struct MarkdownRenderingTests { @Test @@ -219,6 +221,7 @@ struct MarkdownRenderingTests { } } +@MainActor struct OrgRenderingTests { @Test @@ -376,6 +379,7 @@ struct OrgRenderingTests { } } +@MainActor struct RepositoryAssetURLTests { @Test @@ -391,6 +395,7 @@ struct RepositoryAssetURLTests { } } +@MainActor struct RepositoryLinkURLTests { @Test diff --git a/HutchTests/RecentActivityStoreTests.swift b/HutchTests/RecentActivityStoreTests.swift index e6fcc04..7713e6d 100644 --- a/HutchTests/RecentActivityStoreTests.swift +++ b/HutchTests/RecentActivityStoreTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct RecentActivityStoreTests { @Test func removeDropsMatchingEntryKeepingOthers() { diff --git a/HutchTests/RepositoryACLViewModelTests.swift b/HutchTests/RepositoryACLViewModelTests.swift index 9640b93..501074c 100644 --- a/HutchTests/RepositoryACLViewModelTests.swift +++ b/HutchTests/RepositoryACLViewModelTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct RepositoryACLViewModelTests { @Test diff --git a/HutchTests/RepositoryDeployKeyTests.swift b/HutchTests/RepositoryDeployKeyTests.swift index c6ff667..d7a0dad 100644 --- a/HutchTests/RepositoryDeployKeyTests.swift +++ b/HutchTests/RepositoryDeployKeyTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct RepositoryDeployKeyTests { @Test func decodesDeployKey() throws { diff --git a/HutchTests/RepositoryListViewModelTests.swift b/HutchTests/RepositoryListViewModelTests.swift index 5fe3974..7787fca 100644 --- a/HutchTests/RepositoryListViewModelTests.swift +++ b/HutchTests/RepositoryListViewModelTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct RepositoryListViewModelTests { @Test diff --git a/HutchTests/RepositorySettingsViewModelTests.swift b/HutchTests/RepositorySettingsViewModelTests.swift index 13d0b21..25e46c9 100644 --- a/HutchTests/RepositorySettingsViewModelTests.swift +++ b/HutchTests/RepositorySettingsViewModelTests.swift @@ -10,6 +10,7 @@ private struct UpdateRepositoryInfoPayload: Decodable { let id: Int } +@MainActor struct RepositorySettingsViewModelTests { @Test diff --git a/HutchTests/SRHTClientTests.swift b/HutchTests/SRHTClientTests.swift index 12d6d0f..51b6a68 100644 --- a/HutchTests/SRHTClientTests.swift +++ b/HutchTests/SRHTClientTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct SRHTClientTests { @Test diff --git a/HutchTests/SRHTWebURLTests.swift b/HutchTests/SRHTWebURLTests.swift index d1053e6..f20f964 100644 --- a/HutchTests/SRHTWebURLTests.swift +++ b/HutchTests/SRHTWebURLTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct SRHTWebURLTests { private enum Expected { static let chatOrigin = "https://chat.sr.ht" diff --git a/HutchTests/ScopedSearchHistoryStoreTests.swift b/HutchTests/ScopedSearchHistoryStoreTests.swift index 856d69f..8effa06 100644 --- a/HutchTests/ScopedSearchHistoryStoreTests.swift +++ b/HutchTests/ScopedSearchHistoryStoreTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct ScopedSearchHistoryStoreTests { @Test diff --git a/HutchTests/SettingsViewModelTests.swift b/HutchTests/SettingsViewModelTests.swift index 906f33f..e88d064 100644 --- a/HutchTests/SettingsViewModelTests.swift +++ b/HutchTests/SettingsViewModelTests.swift @@ -64,6 +64,7 @@ private struct DeleteResultPayload: Decodable { } @Suite(.serialized) +@MainActor struct SettingsViewModelTests { @Test diff --git a/HutchTests/SettingsViewTests.swift b/HutchTests/SettingsViewTests.swift index 950c3dc..f0f7fc1 100644 --- a/HutchTests/SettingsViewTests.swift +++ b/HutchTests/SettingsViewTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct SettingsViewTests { @Test diff --git a/HutchTests/SourceHutWebDeepLinkMapperTests.swift b/HutchTests/SourceHutWebDeepLinkMapperTests.swift index 0fa3aaa..2c638c2 100644 --- a/HutchTests/SourceHutWebDeepLinkMapperTests.swift +++ b/HutchTests/SourceHutWebDeepLinkMapperTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct SourceHutWebDeepLinkMapperTests { @Test func mapsRepositoryURLs() { diff --git a/HutchTests/SyntaxHighlighterTests.swift b/HutchTests/SyntaxHighlighterTests.swift index 258e81e..3150814 100644 --- a/HutchTests/SyntaxHighlighterTests.swift +++ b/HutchTests/SyntaxHighlighterTests.swift @@ -3,6 +3,7 @@ import Testing import UIKit @testable import Hutch +@MainActor struct SyntaxHighlighterTests { @Test func mapsFileNamesToLanguages() { @@ -24,9 +25,8 @@ struct SyntaxHighlighterTests { let highlighter = SyntaxHighlighter(theme: .light) let html = highlighter.highlightedHTML(for: "let answer = 42", language: "swift") - let unwrapped = try? #require(html) - #expect(unwrapped?.contains("<span style=\"color:") == true) - #expect(unwrapped?.contains("answer") == true) + #expect(html?.contains("<span style=\"color:") == true) + #expect(html?.contains("answer") == true) } @Test @@ -42,7 +42,6 @@ struct SyntaxHighlighterTests { let font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular) let attributed = highlighter.attributedText(for: "print(\"hi\")", language: "python", font: font) - let unwrapped = try? #require(attributed) - #expect(unwrapped?.string.contains("print") == true) + #expect(attributed?.string.contains("print") == true) } } diff --git a/HutchTests/SystemStatusRepositoryTests.swift b/HutchTests/SystemStatusRepositoryTests.swift index 56fa7c0..45d32b9 100644 --- a/HutchTests/SystemStatusRepositoryTests.swift +++ b/HutchTests/SystemStatusRepositoryTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct SystemStatusRepositoryTests { @Test @@ -111,21 +112,21 @@ private struct TestSystemStatusService: SystemStatusServing { } private extension SystemStatusRepositoryTests { - static let cachedSnapshotHTML = #""" + nonisolated static let cachedSnapshotHTML = #""" <div class="component" data-status="disrupted"> <a href="/affected/git.sr.ht/">git.sr.ht</a> <span class="component-status">Disrupted</span> </div> """# - static let cachedOperationalHTML = #""" + nonisolated static let cachedOperationalHTML = #""" <div class="component" data-status="ok"> <a href="/affected/meta.sr.ht/">meta.sr.ht</a> <span class="component-status">Operational</span> </div> """# - static let cachedIncidentRSS = #""" + nonisolated static let cachedIncidentRSS = #""" <rss version="2.0"> <channel> <item> @@ -139,7 +140,7 @@ private extension SystemStatusRepositoryTests { </rss> """# - static let emptyRSS = #""" + nonisolated static let emptyRSS = #""" <rss version="2.0"> <channel></channel> </rss> diff --git a/HutchTests/SystemStatusServiceTests.swift b/HutchTests/SystemStatusServiceTests.swift index d963fa4..9a7af1f 100644 --- a/HutchTests/SystemStatusServiceTests.swift +++ b/HutchTests/SystemStatusServiceTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct SystemStatusServiceTests { private enum Fixture { static let ddosIssueURL = "https://status.sr.ht/issues/2026-04-06-ddos-attack/" diff --git a/HutchTests/TicketBulkActionTests.swift b/HutchTests/TicketBulkActionTests.swift index 2f4d1fd..2c1d738 100644 --- a/HutchTests/TicketBulkActionTests.swift +++ b/HutchTests/TicketBulkActionTests.swift @@ -1,6 +1,7 @@ import Testing @testable import Hutch +@MainActor struct TicketBulkActionTests { @Test diff --git a/HutchTests/TicketDetailViewModelTests.swift b/HutchTests/TicketDetailViewModelTests.swift index ba4a476..d2c0c8e 100644 --- a/HutchTests/TicketDetailViewModelTests.swift +++ b/HutchTests/TicketDetailViewModelTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct TicketDetailViewModelTests { @Test diff --git a/HutchTests/TicketSavedFilterStoreTests.swift b/HutchTests/TicketSavedFilterStoreTests.swift index 2ff64e6..cfdcba6 100644 --- a/HutchTests/TicketSavedFilterStoreTests.swift +++ b/HutchTests/TicketSavedFilterStoreTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct TicketSavedFilterStoreTests { @Test diff --git a/HutchTests/TipStoreViewModelTests.swift b/HutchTests/TipStoreViewModelTests.swift index ed58d1e..734a7cc 100644 --- a/HutchTests/TipStoreViewModelTests.swift +++ b/HutchTests/TipStoreViewModelTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct TipStoreViewModelTests { @Test diff --git a/HutchTests/TrackerListViewModelTests.swift b/HutchTests/TrackerListViewModelTests.swift index 5c57e04..85b051f 100644 --- a/HutchTests/TrackerListViewModelTests.swift +++ b/HutchTests/TrackerListViewModelTests.swift @@ -2,6 +2,7 @@ import Foundation import Testing @testable import Hutch +@MainActor struct TrackerListViewModelTests { @Test diff --git a/HutchTests/TrackerManagementViewModelTests.swift b/HutchTests/TrackerManagementViewModelTests.swift index 531608c..211c838 100644 --- a/HutchTests/TrackerManagementViewModelTests.swift +++ b/HutchTests/TrackerManagementViewModelTests.swift @@ -1,6 +1,7 @@ import Testing @testable import Hutch +@MainActor struct TrackerManagementViewModelTests { @Test diff --git a/HutchWidgetExtension/ContributionGraphWidget.swift b/HutchWidgetExtension/ContributionGraphWidget.swift index 2fc07f2..bc56a03 100644 --- a/HutchWidgetExtension/ContributionGraphWidget.swift +++ b/HutchWidgetExtension/ContributionGraphWidget.swift @@ -28,12 +28,16 @@ struct ContributionGraphTimelineProvider: TimelineProvider { } func getSnapshot(in _: Context, completion: @escaping (ContributionGraphEntry) -> Void) { + // WidgetKit's completion is safe to call from the task; rebind as + // nonisolated(unsafe) so it can cross into the `sending` Task closure. + nonisolated(unsafe) let completion = completion Task { completion(await loadEntry()) } } func getTimeline(in _: Context, completion: @escaping (Timeline<ContributionGraphEntry>) -> Void) { + nonisolated(unsafe) let completion = completion Task { let entry = await loadEntry() let refreshDate = Calendar.contributionCalendar.date(byAdding: .hour, value: 1, to: Date()) ?? Date().addingTimeInterval(3600) diff --git a/ROADMAP.txt b/ROADMAP.txt index 5dbca20..4352b9e 100644 --- a/ROADMAP.txt +++ b/ROADMAP.txt @@ -158,7 +158,7 @@ so "breaking change" does not apply. These buckets track *user-visible scale*. | v3.11.0 | ~~Mailing list subscribe/unsubscribe toggle~~ (shipped) | Ingest-surfaced; state via the `subscriptions` query (the `subscription` field is a trap) | | v3.12.0 | Accessibility | Independent, device-verified | | v4.0.0 | Localization *with* translations | The only true re-presentation | -| — | Swift 6 language mode; cache reads | Internal; ride along, no tag | +| — | ~~Swift 6 language mode~~ (done); cache reads | Internal; ride along, no tag | 3.9.0 was cut this session, bundling the hub.sr.ht writes with the other features listed. That reorders the original plan: the "What's cooking" ingest @@ -384,21 +384,29 @@ rather than a bare `TODO`.) saved-search store for an intent to open. Add the intent once global saved-search persistence exists. -### Swift 6 language mode — no release of its own - -The project builds in Swift 5 language mode with -`SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`. Moving to Swift 6 is blocked on -concurrency diagnostics that are warnings today and errors there: - -- `APICacheTests` and `BundleUserAgentTests` call main-actor-isolated - initialisers and properties from nonisolated contexts, and `await` a few - expressions without marking them. Roughly 20 warnings, all in tests. -- Response types are implicitly `@MainActor` under the default isolation, so - their `Decodable` conformances are too. Decoding one from a nonisolated - context — an `async let` over a raw `client.execute`, say — warns now and - fails then. The pattern that avoids it is `async let` over `@MainActor` - methods, as in `HomeViewModel.loadDashboard` and - `NotificationPreferencesViewModel.load`. +### Swift 6 language mode — done + +`SWIFT_VERSION` is now `6.0` (keeping `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`). +The migration was mostly mechanical, in a few buckets: + +- **Models are `nonisolated`.** Under MainActor-default, every value-type model + was implicitly `@MainActor`; the pure data types in `Models/` (and utility + extensions like `Date.relativeDescription`, `DateFormatter+SRHT`, `SRHTWebURL`) + are now `nonisolated`, so the nonisolated networking layer can use them. +- **App Intents statics.** `AppIntent`/`AppEntity`/`AppEnum` `static var`s were + "global shared mutable state"; the stored ones are now `static let`. +- **Sendable dictionaries.** `nil as String? as Any` in `[String: any Sendable]` + GraphQL variable dicts became `nil as String? as any Sendable`. +- **`UserDefaults`** gets a retroactive `@unchecked Sendable` (documented + thread-safe) since it threads through account sessions and stores. +- **WidgetKit** completion handlers are rebound `nonisolated(unsafe)` to cross + into their `Task {}`; the `@Observable` `TipStoreViewModel` task handle is + `@ObservationIgnored nonisolated(unsafe)` for its nonisolated `deinit`. +- **Tests** run on `@MainActor` (they exercise MainActor app code), with a few + constant fixtures marked `nonisolated` for use inside `@Sendable` stub + closures. + +Builds and the full suite are clean in Swift 6 mode with no behaviour change. ### Cache reads that bypass the client — no release of its own |
