diff --git a/ElementX/Sources/Other/Progress/ProgressMaskModifier.swift b/ElementX/Sources/Other/Progress/ProgressMaskModifier.swift index 7e2c0df31..20ad02c85 100644 --- a/ElementX/Sources/Other/Progress/ProgressMaskModifier.swift +++ b/ElementX/Sources/Other/Progress/ProgressMaskModifier.swift @@ -10,11 +10,11 @@ import SwiftUI extension View { func progressMask(progress: CGFloat, - trackColor: Color = .compound.iconSecondary, - backgroundTrackColor: Color = .compound.iconQuaternary) -> some View { + trackColor: @autoclosure @MainActor () -> Color = .compound.iconSecondary, + backgroundTrackColor: @autoclosure @MainActor () -> Color = .compound.iconQuaternary) -> some View { modifier(ProgressMaskModifier(progress: progress, - trackColor: trackColor, - backgroundTrackColor: backgroundTrackColor)) + trackColor: trackColor(), + backgroundTrackColor: backgroundTrackColor())) } } diff --git a/ElementX/Sources/Other/SwiftUI/Styles/ElementTextFieldStyle.swift b/ElementX/Sources/Other/SwiftUI/Styles/ElementTextFieldStyle.swift index 68aaf2792..8fc8bfed6 100644 --- a/ElementX/Sources/Other/SwiftUI/Styles/ElementTextFieldStyle.swift +++ b/ElementX/Sources/Other/SwiftUI/Styles/ElementTextFieldStyle.swift @@ -10,6 +10,7 @@ import Compound import SwiftUI import SwiftUIIntrospect +@MainActor extension TextFieldStyle where Self == ElementTextFieldStyle { static func element(labelText: String? = nil, footerText: String? = nil, @@ -34,6 +35,7 @@ extension TextFieldStyle where Self == ElementTextFieldStyle { } /// The text field style used in authentication screens. +@MainActor struct ElementTextFieldStyle: @MainActor TextFieldStyle { enum State { case success diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoomInviterLabel.swift b/ElementX/Sources/Other/SwiftUI/Views/RoomInviterLabel.swift index f079023dd..09ab61915 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/RoomInviterLabel.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/RoomInviterLabel.swift @@ -8,6 +8,7 @@ import SwiftUI +@MainActor struct RoomInviterDetails: Equatable { let id: String let displayName: String? diff --git a/ElementX/Sources/Other/SwiftUI/Views/ToolbarButton.swift b/ElementX/Sources/Other/SwiftUI/Views/ToolbarButton.swift index a562fb25a..f8b4c4bb7 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/ToolbarButton.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/ToolbarButton.swift @@ -9,6 +9,7 @@ import Compound import SwiftUI struct ToolbarButton: View { + @MainActor enum Role { static let cancel = Role.cancel(title: L10n.actionCancel) static let done = Role.confirm(title: L10n.actionDone) diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift index b6f407a75..e1a17a58d 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift @@ -128,6 +128,7 @@ struct RoomMembersListScreen: View { } } +@MainActor private enum MembersSection { case joined case invited diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/FormattingToolbar.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/FormattingToolbar.swift index 8625ef444..5ebf7f336 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/FormattingToolbar.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/FormattingToolbar.swift @@ -38,6 +38,7 @@ struct FormattingToolbar: View { } } +@MainActor private extension FormatItem { var foregroundColor: Color { switch state { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 1db931364..9967fbcbc 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -109,6 +109,7 @@ enum RoomScreenFooterViewDetails { case verificationViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL) } +@MainActor enum PinnedEventsBannerState: Equatable { case loading(numbersOfEvents: Int) case loaded(state: PinnedEventsState) @@ -213,6 +214,7 @@ enum PinnedEventsBannerState: Equatable { } } +@MainActor struct PinnedEventsState: Equatable { var pinnedEventContents: OrderedDictionary = [:] { didSet { diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift index aa77399f6..fcec60475 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift @@ -243,6 +243,7 @@ private struct VerifiedUserSendFailureView: View { } } +@MainActor private extension EncryptionAuthenticity { var foregroundStyle: SwiftUI.Color { switch color { diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbleBackground.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbleBackground.swift index 98de4374d..cdcf88d4f 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbleBackground.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbleBackground.swift @@ -16,10 +16,10 @@ extension View { /// - color: self explanatory, defaults to subtle secondary func bubbleBackground(isOutgoing: Bool = true, insets: EdgeInsets = .init(top: 8, leading: 12, bottom: 8, trailing: 12), - color: Color? = .compound.bgSubtleSecondary) -> some View { + color: @autoclosure @MainActor () -> Color? = .compound.bgSubtleSecondary) -> some View { modifier(TimelineItemBubbleBackgroundModifier(isOutgoing: isOutgoing, insets: insets, - color: color)) + color: color())) } } diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index f56906fcc..2949f8184 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -238,6 +238,7 @@ struct TimelineItemBubbledStylerView: View { } } +@MainActor private extension EventBasedTimelineItemProtocol { var bubbleBackgroundColor: Color? { let defaultColor: Color = isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift index a635bf30d..0cc65be2d 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift @@ -112,6 +112,7 @@ private struct TimelineItemSendInfoLabel: View { } /// All the data needed to render a timeline item's send info label. +@MainActor private struct TimelineItemSendInfo { enum Status { case sendingFailed @@ -185,6 +186,7 @@ private extension TimelineItemSendInfo { } } +@MainActor private extension EncryptionAuthenticity { var foregroundStyle: SwiftUI.Color { switch color { diff --git a/UnitTests/Sources/PinnedEventsBannerStateTests.swift b/UnitTests/Sources/PinnedEventsBannerStateTests.swift index 9f931fca2..2baa96fbe 100644 --- a/UnitTests/Sources/PinnedEventsBannerStateTests.swift +++ b/UnitTests/Sources/PinnedEventsBannerStateTests.swift @@ -9,6 +9,7 @@ @testable import ElementX import XCTest +@MainActor class PinnedEventsBannerStateTests: XCTestCase { func testEmpty() { var state = PinnedEventsBannerState.loading(numbersOfEvents: 0) diff --git a/compound-ios/Package.swift b/compound-ios/Package.swift index da6050c44..9022c76a5 100644 --- a/compound-ios/Package.swift +++ b/compound-ios/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.2 import PackageDescription @@ -22,6 +22,9 @@ let package = Package( .product(name: "CompoundDesignTokens", package: "compound-design-tokens"), .product(name: "SwiftUIIntrospect", package: "SwiftUI-Introspect"), .product(name: "SFSafeSymbols", package: "SFSafeSymbols") + ], + swiftSettings: [ + .defaultIsolation(MainActor.self) ] ), .testTarget( diff --git a/compound-ios/Sources/Compound/Colors/CompoundColors.swift b/compound-ios/Sources/Compound/Colors/CompoundColors.swift index f82d3a8db..0a63ac12d 100644 --- a/compound-ios/Sources/Compound/Colors/CompoundColors.swift +++ b/compound-ios/Sources/Compound/Colors/CompoundColors.swift @@ -42,7 +42,7 @@ public class CompoundColors { /// Customise the colour at the specified key path with the supplied colour. /// Supplying `nil` as the colour will remove any existing customisation. - @MainActor public func override(_ keyPath: KeyPath, with color: Color?) { + public func override(_ keyPath: KeyPath, with color: Color?) { overrides[keyPath] = color } diff --git a/compound-ios/Sources/Compound/Colors/CompoundUIColors.swift b/compound-ios/Sources/Compound/Colors/CompoundUIColors.swift index c23fea3af..88879ad5c 100644 --- a/compound-ios/Sources/Compound/Colors/CompoundUIColors.swift +++ b/compound-ios/Sources/Compound/Colors/CompoundUIColors.swift @@ -15,10 +15,12 @@ public extension UIColor { } /// The colours used by Element as defined in Compound Design Tokens. -/// This struct contains only the colour tokens in a more usable form. +/// This class contains only the colour tokens in a more usable form. +/// Since this can be used by attributed strings which may run in non isolated concurrent contexts, +/// The object needs to be nonisolated. @Observable @dynamicMemberLookup -public class CompoundUIColors { +public final nonisolated class CompoundUIColors { /// The base colour tokens that form the palette of available colours. /// /// Normally these shouldn't be necessary, however in practice we may need @@ -35,7 +37,7 @@ public class CompoundUIColors { /// Customise the colour at the specified key path with the supplied colour. /// Supplying `nil` as the colour will remove any existing customisation. - @MainActor public func override(_ keyPath: KeyPath, with color: UIColor?) { + public func override(_ keyPath: KeyPath, with color: UIColor?) { overrides[keyPath] = color } @@ -44,7 +46,10 @@ public class CompoundUIColors { // swiftformat:disable numberFormatting /// This token is a placeholder and hasn't been finalised. @available(iOS, deprecated: 100000.0, message: "This token should be generated by now.") - public let _bgCodeBlock = coreTokens.gray100 + public var _bgCodeBlock: UIColor { + CompoundUIColors.coreTokens.gray100 + } + /// This token is a placeholder and hasn't been finalised. @available(iOS, deprecated: 100000.0, message: "This token should be generated by now.") public let _bgSubtleSecondaryAlpha = coreTokens.alphaGray300 diff --git a/compound-ios/Sources/Compound/Text Field Styles/SearchFieldStyle.swift b/compound-ios/Sources/Compound/Text Field Styles/SearchFieldStyle.swift index b60b3e9d7..410d84d72 100644 --- a/compound-ios/Sources/Compound/Text Field Styles/SearchFieldStyle.swift +++ b/compound-ios/Sources/Compound/Text Field Styles/SearchFieldStyle.swift @@ -11,7 +11,6 @@ import SwiftUIIntrospect public extension View { /// Styles a search bar text field using the Compound design tokens. /// This modifier is to be used in combination with `.searchable`. - @MainActor func compoundSearchField() -> some View { introspect(.navigationStack, on: .supportedVersions, scope: .ancestor) { navigationController in // Uses the navigation stack as .searchField is unreliable when pushing the second search bar, during the create rooms flow. diff --git a/compound-ios/Tests/CompoundTests/AvatarColorsTests.swift b/compound-ios/Tests/CompoundTests/AvatarColorsTests.swift index 1b8c1c2ea..ab6cbee1d 100644 --- a/compound-ios/Tests/CompoundTests/AvatarColorsTests.swift +++ b/compound-ios/Tests/CompoundTests/AvatarColorsTests.swift @@ -11,6 +11,7 @@ import Foundation import SwiftUI import XCTest +@MainActor final class DecorativeColorsTests: XCTestCase { struct TestCase { let input: String diff --git a/compound-ios/Tests/CompoundTests/FontSizeTests.swift b/compound-ios/Tests/CompoundTests/FontSizeTests.swift index 4bd8dd15a..5b6c123ed 100644 --- a/compound-ios/Tests/CompoundTests/FontSizeTests.swift +++ b/compound-ios/Tests/CompoundTests/FontSizeTests.swift @@ -10,6 +10,7 @@ import SwiftUI import XCTest +@MainActor final class FontSizeTests: XCTestCase { /// Test all system text styles to assert mapping between `Font` and `UIFont`. func testTextStyle() { @@ -103,3 +104,4 @@ final class FontSizeTests: XCTestCase { XCTAssertEqual(styledCustomFootnote15FontSize?.style, .footnote) } } + \ No newline at end of file diff --git a/compound-ios/Tests/CompoundTests/OverrideColorTests.swift b/compound-ios/Tests/CompoundTests/OverrideColorTests.swift index 3d489a50e..2e6ff41a3 100644 --- a/compound-ios/Tests/CompoundTests/OverrideColorTests.swift +++ b/compound-ios/Tests/CompoundTests/OverrideColorTests.swift @@ -10,20 +10,25 @@ import Foundation import XCTest -@MainActor -class OverrideColorTests: XCTestCase { - func testSwiftUI() { - let colors = CompoundColors() - let tokens = CompoundColorTokens() - XCTAssertEqual(colors.textPrimary, tokens.textPrimary) - - colors.override(\.textPrimary, with: .pink) - XCTAssertEqual(colors.textPrimary, .pink) - - colors.override(\.textPrimary, with: nil) - XCTAssertEqual(colors.textPrimary, tokens.textPrimary) +final class OverrideColorTests: XCTestCase { + func testSwiftUI() async { + // For some very weird reason we need this to be async, `@MainActor` is not enough + // it will compile but when running it will crash at the end of the run due to some deinit problems. + // The other solution would be to make CompoundColors nonisolated but we don't really need that. + await MainActor.run { + let colors = CompoundColors() + let tokens = CompoundColorTokens() + XCTAssertEqual(colors.textPrimary, tokens.textPrimary) + + colors.override(\.textPrimary, with: .pink) + XCTAssertEqual(colors.textPrimary, .pink) + + colors.override(\.textPrimary, with: nil) + XCTAssertEqual(colors.textPrimary, tokens.textPrimary) + } } + /// UIColors are nonisolated, so this is fine. func testUIKit() { let colors = CompoundUIColors() let tokens = CompoundUIColorTokens() diff --git a/compound-ios/Tests/CompoundTests/PreviewTests.swift b/compound-ios/Tests/CompoundTests/PreviewTests.swift index b896daa6f..fcf544d15 100644 --- a/compound-ios/Tests/CompoundTests/PreviewTests.swift +++ b/compound-ios/Tests/CompoundTests/PreviewTests.swift @@ -8,7 +8,7 @@ import Combine @testable import Compound -@testable import SnapshotTesting +@preconcurrency @testable import SnapshotTesting import SwiftUI import XCTest @@ -28,15 +28,17 @@ class PreviewTests: XCTestCase { .init(name: "iPad", device: "iPad")] private var recordMode: SnapshotTestingConfiguration.Record = .missing - override func setUp() { - super.setUp() + override func setUp() async throws { + try await super.setUp() - if ProcessInfo().environment["RECORD_FAILURES"].map(Bool.init) == true { - recordMode = .failed - } + await MainActor.run { + if ProcessInfo().environment["RECORD_FAILURES"].map(Bool.init) == true { + recordMode = .failed + } - checkEnvironments() - UIView.setAnimationsEnabled(false) + checkEnvironments() + UIView.setAnimationsEnabled(false) + } } /// Check environments to avoid problems with snapshots on different devices or OS. @@ -183,6 +185,7 @@ private extension PreviewDevice { } private extension Snapshotting where Value: SwiftUI.View, Format == UIImage { + @MainActor static func prefireImage(drawHierarchyInKeyWindow: Bool = false, preferences: SnapshotPreferences, layout: SwiftUISnapshotLayout = .sizeThatFits, @@ -204,7 +207,7 @@ private extension Snapshotting where Value: SwiftUI.View, Format == UIImage { } return SimplySnapshotting(pathExtension: "png", diffing: .prefireImage(preferences: preferences, scale: traits.displayScale)) - .asyncPullback { view in + .asyncPullback { @MainActor view in var config = config let controller: UIViewController