Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 82 additions & 64 deletions Sources/SwiftNavigation/TextState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
47 changes: 25 additions & 22 deletions Tests/SwiftNavigationTests/ButtonStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading