diff --git a/Sources/SwiftNavigation/TextState.swift b/Sources/SwiftNavigation/TextState.swift index abeb154eb..d222b2266 100644 --- a/Sources/SwiftNavigation/TextState.swift +++ b/Sources/SwiftNavigation/TextState.swift @@ -126,28 +126,36 @@ public struct TextState: Equatable, Hashable, Sendable { comment: StaticString? ) #endif - case localizedStringResource(LocalizedStringResourceBox) + #if canImport(Darwin) + case localizedStringResource(LocalizedStringResourceBox) + #endif case verbatim(String) static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case (.concatenated(let l1, let l2), .concatenated(let r1, let r2)): return l1 == r1 && l2 == r2 - case (.concatenated, .localizedStringResource), - (.localizedStringResource, .concatenated), - (.concatenated, .verbatim), + #if canImport(Darwin) + case (.concatenated, .localizedStringResource), + (.localizedStringResource, .concatenated): + // NB: We do not attempt to equate concatenated cases. + return false + #endif + case (.concatenated, .verbatim), (.verbatim, .concatenated): // NB: We do not attempt to equate concatenated cases. return false case (.verbatim(let lhs), .verbatim(let rhs)): return lhs == rhs - case (.verbatim(let string), .localizedStringResource(let resource)), - (.localizedStringResource(let resource), .verbatim(let string)): - return string == resource.asString() + #if canImport(Darwin) + case (.verbatim(let string), .localizedStringResource(let resource)), + (.localizedStringResource(let resource), .verbatim(let string)): + return string == resource.asString() - case (.localizedStringResource(let lhs), .localizedStringResource(let rhs)): - return lhs.asString() == rhs.asString() + case (.localizedStringResource(let lhs), .localizedStringResource(let rhs)): + return lhs.asString() == rhs.asString() + #endif #if canImport(SwiftUI) case (.concatenated, .localizedStringKey), @@ -193,8 +201,10 @@ public struct TextState: Equatable, Hashable, Sendable { hasher.combine(key.formatted(tableName: tableName, bundle: bundle, comment: comment)) #endif - case .localizedStringResource(let resource): - hasher.combine(resource.asString()) + #if canImport(Darwin) + case .localizedStringResource(let resource): + hasher.combine(resource.asString()) + #endif case .verbatim(let string): hasher.combine(string) @@ -205,41 +215,43 @@ public struct TextState: Equatable, Hashable, Sendable { // MARK: - LocalizedStringResourceBox -private struct LocalizedStringResourceBox: @unchecked Sendable { - // REVISIT: Make 'Any' into 'any Sendable' when minimum deployment target is iOS 18 - let value: Any - - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) - init(_ resource: LocalizedStringResource) { - self.value = resource - } +#if canImport(Darwin) + private struct LocalizedStringResourceBox: @unchecked Sendable { + // REVISIT: Make 'Any' into 'any Sendable' when minimum deployment target is iOS 18 + let value: Any - func asText() -> Text { - guard - #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *), - let resource = value as? LocalizedStringResource - else { - preconditionFailure( - "LocalizedStringResourceBox should only be exposed where LocalizedStringResource is available." - ) + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + init(_ resource: LocalizedStringResource) { + self.value = resource } - return Text(resource) - } + func asText() -> Text { + guard + #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *), + let resource = value as? LocalizedStringResource + else { + preconditionFailure( + "LocalizedStringResourceBox should only be exposed where LocalizedStringResource is available." + ) + } - func asString() -> String { - guard - #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *), - let resource = value as? LocalizedStringResource - else { - preconditionFailure( - "LocalizedStringResourceBox should only be exposed where LocalizedStringResource is available." - ) + return Text(resource) } - return String(localized: resource) + func asString() -> String { + guard + #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *), + let resource = value as? LocalizedStringResource + else { + preconditionFailure( + "LocalizedStringResourceBox should only be exposed where LocalizedStringResource is available." + ) + } + + return String(localized: resource) + } } -} +#endif // MARK: - API @@ -269,14 +281,16 @@ extension TextState { } #endif - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) - public init( - _ resource: LocalizedStringResource - ) { - self.storage = .localizedStringResource( - LocalizedStringResourceBox(resource) - ) - } + #if canImport(Darwin) + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + public init( + _ resource: LocalizedStringResource + ) { + self.storage = .localizedStringResource( + LocalizedStringResourceBox(resource) + ) + } + #endif public static func + (lhs: Self, rhs: Self) -> Self { .init(storage: .concatenated(lhs, rhs)) @@ -554,19 +568,19 @@ extension TextState { } case .accessibilityLabel(let value): if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - switch value.storage { - case .verbatim(let string): - return text.accessibilityLabel(string) - case .localizedStringKey(let key, let tableName, let bundle, let comment): - return text.accessibilityLabel( - Text(key, tableName: tableName, bundle: bundle, comment: comment) - ) - case .localizedStringResource(let resourceBox): - return text.accessibilityLabel( - resourceBox.asText() - ) - case .concatenated(_, _): - assertionFailure("`.accessibilityLabel` does not support concatenated `TextState`") + switch value.storage { + case .verbatim(let string): + return text.accessibilityLabel(string) + case .localizedStringKey(let key, let tableName, let bundle, let comment): + return text.accessibilityLabel( + Text(key, tableName: tableName, bundle: bundle, comment: comment) + ) + case .localizedStringResource(let resourceBox): + return text.accessibilityLabel( + resourceBox.asText() + ) + case .concatenated(_, _): + assertionFailure("`.accessibilityLabel` does not support concatenated `TextState`") return text } } else { @@ -678,8 +692,10 @@ extension String { ) #endif - case .localizedStringResource(let resourceBox): - self = resourceBox.asString() + #if canImport(Darwin) + case .localizedStringResource(let resourceBox): + self = resourceBox.asString() + #endif case .verbatim(let string): self = string @@ -740,8 +756,10 @@ extension TextState: CustomDumpRepresentable { case .localizedStringKey(let key, let tableName, let bundle, let comment): output = key.formatted(tableName: tableName, bundle: bundle, comment: comment) #endif - case .localizedStringResource(let resourceBox): - output = resourceBox.asString() + #if canImport(Darwin) + case .localizedStringResource(let resourceBox): + output = resourceBox.asString() + #endif case .verbatim(let string): output = string diff --git a/Tests/SwiftNavigationTests/ButtonStateTests.swift b/Tests/SwiftNavigationTests/ButtonStateTests.swift index de515badc..0b68eeec0 100644 --- a/Tests/SwiftNavigationTests/ButtonStateTests.swift +++ b/Tests/SwiftNavigationTests/ButtonStateTests.swift @@ -4,32 +4,35 @@ import Testing struct ButtonStateTests { - @Test - func testAsyncAnimationWarning() async { - let button = ButtonState(action: .send((), animation: .easeInOut)) { - TextState("Animate!") - } - await withKnownIssue { - await button.withAction { _ in - await Task.yield() + #if canImport(SwiftUI) + @Test + func testAsyncAnimationWarning() async { + let button = ButtonState(action: .send((), animation: .easeInOut)) { + TextState("Animate!") } - } matching: { issue in - issue.description.hasSuffix( - """ - An animated action was performed asynchronously: … - Action: - ButtonStateAction.send( - (), - animation: Animation.easeInOut - ) + await withKnownIssue { + await button.withAction { _ in + await Task.yield() + } + } matching: { issue in + issue.description.hasSuffix( + """ + An animated action was performed asynchronously: … + + Action: + ButtonStateAction.send( + (), + animation: Animation.easeInOut + ) - Asynchronous actions cannot be animated. Evaluate this action in a synchronous closure, \ - or use 'SwiftUI.withAnimation' explicitly. - """ - ) + Asynchronous actions cannot be animated. Evaluate this action in a synchronous closure, \ + or use 'SwiftUI.withAnimation' explicitly. + """ + ) + } } - } + #endif } #endif diff --git a/Tests/SwiftNavigationTests/TextStateTests.swift b/Tests/SwiftNavigationTests/TextStateTests.swift index ae0a2decb..0a465ce7c 100644 --- a/Tests/SwiftNavigationTests/TextStateTests.swift +++ b/Tests/SwiftNavigationTests/TextStateTests.swift @@ -14,76 +14,80 @@ final class TextStateTests: XCTestCase { """ ) - dump = "" - customDump( - TextState("Hello, ") - + TextState("world").bold().italic() - + TextState("!"), - to: &dump - ) - XCTAssertEqual( - dump, - """ - "Hello, _**world**_!" - """ - ) + #if canImport(Darwin) + dump = "" + customDump( + TextState("Hello, ") + + TextState("world").bold().italic() + + TextState("!"), + to: &dump + ) + XCTAssertEqual( + dump, + """ + "Hello, _**world**_!" + """ + ) - dump = "" - customDump( - TextState("Offset by 10.5").baselineOffset(10.5) - + TextState("\n") + TextState("Headline").font(.headline) - + TextState("\n") + TextState("No font").font(nil) - + TextState("\n") + TextState("Light font weight").fontWeight(.light) - + TextState("\n") + TextState("No font weight").fontWeight(nil) - + TextState("\n") + TextState("Red").foregroundColor(.red) - + TextState("\n") + TextState("No color").foregroundColor(nil) - + TextState("\n") + TextState("Italic").italic() - + TextState("\n") + TextState("Kerning of 2.5").kerning(2.5) - + TextState("\n") + TextState("Stricken").strikethrough() - + TextState("\n") + TextState("Stricken green").strikethrough(color: .green) - + TextState("\n") + TextState("Not stricken blue").strikethrough(false, color: .blue) - + TextState("\n") + TextState("Tracking of 5.5").tracking(5.5) - + TextState("\n") + TextState("Underlined").underline() - + TextState("\n") + TextState("Underlined pink").underline(color: .pink) - + TextState("\n") + TextState("Not underlined purple").underline(false, color: .pink), - to: &dump - ) - expectNoDifference( - dump, - #""" - """ - Offset by 10.5 - Headline - No font - Light font weight - No font weight - Red - No color - _Italic_ - Kerning of 2.5 - ~~Stricken~~ - Stricken green - Not stricken blue - Tracking of 5.5 - Underlined - Underlined pink - Not underlined purple - """ - """# - ) + dump = "" + customDump( + TextState("Offset by 10.5").baselineOffset(10.5) + + TextState("\n") + TextState("Headline").font(.headline) + + TextState("\n") + TextState("No font").font(nil) + + TextState("\n") + TextState("Light font weight").fontWeight(.light) + + TextState("\n") + TextState("No font weight").fontWeight(nil) + + TextState("\n") + TextState("Red").foregroundColor(.red) + + TextState("\n") + TextState("No color").foregroundColor(nil) + + TextState("\n") + TextState("Italic").italic() + + TextState("\n") + TextState("Kerning of 2.5").kerning(2.5) + + TextState("\n") + TextState("Stricken").strikethrough() + + TextState("\n") + TextState("Stricken green").strikethrough(color: .green) + + TextState("\n") + TextState("Not stricken blue").strikethrough(false, color: .blue) + + TextState("\n") + TextState("Tracking of 5.5").tracking(5.5) + + TextState("\n") + TextState("Underlined").underline() + + TextState("\n") + TextState("Underlined pink").underline(color: .pink) + + TextState("\n") + TextState("Not underlined purple").underline(false, color: .pink), + to: &dump + ) + expectNoDifference( + dump, + #""" + """ + Offset by 10.5 + Headline + No font + Light font weight + No font weight + Red + No color + _Italic_ + Kerning of 2.5 + ~~Stricken~~ + Stricken green + Not stricken blue + Tracking of 5.5 + Underlined + Underlined pink + Not underlined purple + """ + """# + ) + #endif } - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) - func testTextStateLocalizedStringResource() { - var dump = "" - let resource = LocalizedStringResource("hello.world", defaultValue: "Hello, world!") - customDump(TextState(resource), to: &dump) - XCTAssertEqual( - dump, - """ - "Hello, world!" - """ - ) - } + #if canImport(Darwin) + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + func testTextStateLocalizedStringResource() { + var dump = "" + let resource = LocalizedStringResource("hello.world", defaultValue: "Hello, world!") + customDump(TextState(resource), to: &dump) + XCTAssertEqual( + dump, + """ + "Hello, world!" + """ + ) + } + #endif #endif }