Compound - Swift 6.2 and Main actor isolation (#5109)

* compound is now using 6.2 and main actor isolation + some tweaks to make it build tests and EX

* better handling of the nonisolated context for CompoundUIColors

* improving docs

* fix comment

* remove unused Sendable conformance
This commit is contained in:
Mauro
2026-02-16 11:19:12 +01:00
committed by GitHub
parent a50738db64
commit 77c88a817a
20 changed files with 65 additions and 34 deletions

View File

@@ -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(

View File

@@ -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<CompoundColorTokens, Color>, with color: Color?) {
public func override(_ keyPath: KeyPath<CompoundColorTokens, Color>, with color: Color?) {
overrides[keyPath] = color
}

View File

@@ -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<CompoundUIColorTokens, UIColor>, with color: UIColor?) {
public func override(_ keyPath: KeyPath<CompoundUIColorTokens, UIColor>, 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

View File

@@ -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.

View File

@@ -11,6 +11,7 @@ import Foundation
import SwiftUI
import XCTest
@MainActor
final class DecorativeColorsTests: XCTestCase {
struct TestCase {
let input: String

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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<UIImage>(pathExtension: "png", diffing: .prefireImage(preferences: preferences, scale: traits.displayScale))
.asyncPullback { view in
.asyncPullback { @MainActor view in
var config = config
let controller: UIViewController