diff --git a/Sources/SkipUI/SkipUI/Commands/Menu.swift b/Sources/SkipUI/SkipUI/Commands/Menu.swift index 525017f..d3c1c0c 100644 --- a/Sources/SkipUI/SkipUI/Commands/Menu.swift +++ b/Sources/SkipUI/SkipUI/Commands/Menu.swift @@ -126,23 +126,16 @@ public final class Menu : View { @Composable static func ComposeDropdownMenuItems(for itemViews: [View], selection: Hashable? = nil, context: ComposeContext, replaceMenu: (Menu?) -> Void) { for itemView in itemViews { - if var strippedItemView = itemView.strippingModifiers(perform: { $0 }) { - if let shareLink = strippedItemView as? ShareLink { - shareLink.ComposeAction() - strippedItemView = shareLink.content - } else if let link = strippedItemView as? Link { - link.ComposeAction() - strippedItemView = link.content - } - if let button = strippedItemView as? Button { + if let strippedItemView = itemView.strippingModifiers(perform: { $0 }) { + if let buttonRep = strippedItemView as? ButtonRepresentable { let isSelected: Bool? if let tagView = itemView as? TagModifierView, tagView.role == ComposeModifierRole.tag { isSelected = tagView.value == selection } else { isSelected = nil } - ComposeDropdownMenuItem(for: button.label, context: context, isSelected: isSelected) { - button.action() + ComposeDropdownMenuItem(for: buttonRep.makeComposeLabel(), context: context, isSelected: isSelected) { + buttonRep.action() replaceMenu(nil) } } else if let text = strippedItemView as? Text { diff --git a/Sources/SkipUI/SkipUI/Components/Link.swift b/Sources/SkipUI/SkipUI/Components/Link.swift index 5f41fae..ec1ffc1 100644 --- a/Sources/SkipUI/SkipUI/Components/Link.swift +++ b/Sources/SkipUI/SkipUI/Components/Link.swift @@ -8,24 +8,31 @@ import androidx.compose.runtime.Composable // Use a class to be able to update our openURL action on compose by reference. // SKIP @bridge -public final class Link : View { - let content: Button +public final class Link : View, ButtonRepresentable { + var action: () -> Void + let label: ComposeBuilder + let role: ButtonRole? = nil + var openURL = OpenURLAction.default public init(destination: URL, @ViewBuilder label: () -> any View) { #if SKIP - content = Button(action: { self.openURL(destination) }, label: label) + self.action = { self.openURL(destination) } + self.label = ComposeBuilder.from(label) #else - content = Button("", action: {}) + self.action = {} + self.label = ComposeBuilder(view: EmptyView()) #endif } // SKIP @bridge public init(destination: URL, bridgedLabel: any View) { #if SKIP - content = Button(bridgedRole: nil, action: { self.openURL(destination) }, bridgedLabel: bridgedLabel) + self.action = { self.openURL(destination) } + self.label = ComposeBuilder.from { bridgedLabel } #else - content = Button("", action: {}) + self.action = {} + self.label = ComposeBuilder(view: EmptyView()) #endif } @@ -39,12 +46,13 @@ public final class Link : View { #if SKIP @Composable override func ComposeContent(context: ComposeContext) { - ComposeAction() - content.Compose(context: context) + let label = makeComposeLabel() + Button(action: action, label: { label }).Compose(context: context) } - @Composable func ComposeAction() { + @Composable func makeComposeLabel() -> ComposeBuilder { openURL = EnvironmentValues.shared.openURL + return label } #else public var body: some View { diff --git a/Sources/SkipUI/SkipUI/Components/ShareLink.swift b/Sources/SkipUI/SkipUI/Components/ShareLink.swift index c5d73b9..34891ed 100644 --- a/Sources/SkipUI/SkipUI/Components/ShareLink.swift +++ b/Sources/SkipUI/SkipUI/Components/ShareLink.swift @@ -10,14 +10,15 @@ import androidx.core.content.ContextCompat.startActivity #endif // Use a class to be able to update our openURL action on compose by reference. -public final class ShareLink : View { +public final class ShareLink : View, ButtonRepresentable { private static let defaultSystemImageName = "square.and.arrow.up" let text: Text let subject: Text? let message: Text? - let content: Button var action: () -> Void + let label: ComposeBuilder + let role: ButtonRole? = nil init(text: Text, subject: Text? = nil, message: Text? = nil, @ViewBuilder label: () -> any View) { self.text = text @@ -25,9 +26,9 @@ public final class ShareLink : View { self.message = message self.action = { } #if SKIP - self.content = Button(action: { self.action() }, label: label) + self.label = ComposeBuilder.from(label) #else - self.content = Button("", action: {}) + self.label = ComposeBuilder(view: EmptyView()) #endif } @@ -40,14 +41,12 @@ public final class ShareLink : View { } public convenience init(item: URL, subject: Text? = nil, message: Text? = nil) { - self.init(text: Text(item.absoluteString), subject: subject, message: message) { - Image(systemName: Self.defaultSystemImageName) - } + self.init(item: item.absoluteString, subject: subject, message: message) } public convenience init(item: String, subject: Text? = nil, message: Text? = nil) { self.init(text: Text(item), subject: subject, message: message) { - Image(systemName: Self.defaultSystemImageName) + Label("Share...", systemImage: Self.defaultSystemImageName) } } @@ -89,11 +88,11 @@ public final class ShareLink : View { #if SKIP @Composable override func ComposeContent(context: ComposeContext) { - ComposeAction() - content.Compose(context: context) + let label = makeComposeLabel() + Button(action: action, label: { label }).Compose(context: context) } - @Composable func ComposeAction() { + @Composable func makeComposeLabel() -> ComposeBuilder { let localContext = LocalContext.current let intent = Intent().apply { @@ -109,6 +108,7 @@ public final class ShareLink : View { let shareIntent = Intent.createChooser(intent, nil) localContext.startActivity(shareIntent) } + return label } #else public var body: some View { diff --git a/Sources/SkipUI/SkipUI/Compose/ComposeBuilder.swift b/Sources/SkipUI/SkipUI/Compose/ComposeBuilder.swift index 1e3e2e0..d0a4dfd 100644 --- a/Sources/SkipUI/SkipUI/Compose/ComposeBuilder.swift +++ b/Sources/SkipUI/SkipUI/Compose/ComposeBuilder.swift @@ -83,6 +83,12 @@ public struct ComposeBuilder: View { content(viewCollectingContext) return views } + + @Composable func textRepresentation(context: ComposeContext) -> Text? { + return collectViews(context: context).compactMap { + $0.strippingModifiers { $0 as? TextRepresentable } + }.first?.textRepresentation(context: context) + } #else public var body: some View { stubView() diff --git a/Sources/SkipUI/SkipUI/Containers/Navigation.swift b/Sources/SkipUI/SkipUI/Containers/Navigation.swift index b0049dd..756b549 100644 --- a/Sources/SkipUI/SkipUI/Containers/Navigation.swift +++ b/Sources/SkipUI/SkipUI/Containers/Navigation.swift @@ -267,7 +267,7 @@ public struct NavigationStack : View { } return } - + let toolbarItems = ToolbarItems(content: toolbarContent.value.reduced.content ?? []) let topLeadingItems = toolbarItems.filterTopBarLeading() let topTrailingItems = toolbarItems.filterTopBarTrailing() @@ -1139,10 +1139,13 @@ struct NavigationTitlePreferenceKey: PreferenceKey { #endif // SKIP @bridge -public struct NavigationLink : View, ListItemAdapting { +public struct NavigationLink : View, ListItemAdapting, ButtonRepresentable { let value: Any? let destination: ComposeBuilder? + + var action: () -> Void = {} let label: ComposeBuilder + let role: ButtonRole? = nil private static let minimumNavigationInterval = 0.35 private static var lastNavigationTime = 0.0 @@ -1187,6 +1190,12 @@ public struct NavigationLink : View, ListItemAdapting { Button.ComposeButton(label: label, context: context, isEnabled: isNavigationEnabled(), action: navigationAction()) } + @Composable func makeComposeLabel() -> ComposeBuilder { + let navigator = LocalNavigator.current + action = navigationAction() + return label + } + @Composable func shouldComposeListItem() -> Bool { let buttonStyle = EnvironmentValues.shared._buttonStyle return buttonStyle == nil || buttonStyle == .automatic || buttonStyle == .plain @@ -1213,7 +1222,7 @@ public struct NavigationLink : View, ListItemAdapting { return (value != nil || destination != nil) && EnvironmentValues.shared.isEnabled } - @Composable internal func navigationAction() -> () -> Void { + @Composable func navigationAction() -> () -> Void { let navigator = LocalNavigator.current return { // Hack to prevent multiple quick taps from pushing duplicate entries diff --git a/Sources/SkipUI/SkipUI/Controls/Button.swift b/Sources/SkipUI/SkipUI/Controls/Button.swift index 8b42158..a890c92 100644 --- a/Sources/SkipUI/SkipUI/Controls/Button.swift +++ b/Sources/SkipUI/SkipUI/Controls/Button.swift @@ -25,9 +25,18 @@ import struct CoreGraphics.CGFloat import struct CoreGraphics.CGRect #endif +protocol ButtonRepresentable { + var action: () -> Void { get set } + var label: ComposeBuilder { get } + var role: ButtonRole? { get } +#if SKIP + @Composable func makeComposeLabel() -> ComposeBuilder +#endif +} + // SKIP @bridge -public struct Button : View, ListItemAdapting { - let action: () -> Void +public struct Button : View, ListItemAdapting, ButtonRepresentable, TextRepresentable { + var action: () -> Void let label: ComposeBuilder let role: ButtonRole? @@ -77,6 +86,14 @@ public struct Button : View, ListItemAdapting { Self.ComposeButton(label: label, context: context, role: role, action: action) } + @Composable func textRepresentation(context: ComposeContext) -> Text? { + return label.textRepresentation(context: context) + } + + @Composable func makeComposeLabel() -> ComposeBuilder { + return label + } + @Composable func shouldComposeListItem() -> Bool { let buttonStyle = EnvironmentValues.shared._buttonStyle return buttonStyle == nil || buttonStyle == .automatic || buttonStyle == .plain diff --git a/Sources/SkipUI/SkipUI/Layout/Presentation.swift b/Sources/SkipUI/SkipUI/Layout/Presentation.swift index 4039350..9c41742 100644 --- a/Sources/SkipUI/SkipUI/Layout/Presentation.swift +++ b/Sources/SkipUI/SkipUI/Layout/Presentation.swift @@ -213,8 +213,8 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { } else { actionViews = [actions] } - let composableActions: [View] = actionViews.compactMap { - $0.strippingModifiers { $0 as? Button ?? $0 as? Link ?? $0 as? NavigationLink } + let buttonViews = actionViews.compactMap { + $0.strippingModifiers { $0 as? ButtonRepresentable } } let messageViews: [View] if let composeBuilder = message as? ComposeBuilder { @@ -225,8 +225,8 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { messageViews = [] } let messageText = messageViews.compactMap { - $0.strippingModifiers { $0 as? Text } - }.first + $0.strippingModifiers { $0 as? TextRepresentable } + }.first?.textRepresentation(context: context) ModalBottomSheet(onDismissRequest: { isPresented.set(false) }, sheetState: sheetState, containerColor: androidx.compose.ui.graphics.Color.Transparent, dragHandle: nil, contentWindowInsets: { WindowInsets(0.dp, 0.dp, 0.dp, 0.dp) }) { // Add padding to always keep the sheet away from the top of the screen. It should tap to dismiss like the background @@ -246,7 +246,7 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { .verticalScroll(scrollState) let contentContext = context.content(stateSaver: stateSaver) Column(modifier: modifier, horizontalAlignment: androidx.compose.ui.Alignment.CenterHorizontally) { - ComposeConfirmationDialog(title: title, context: contentContext, isPresented: isPresented, actionViews: composableActions, message: messageText) + ComposeConfirmationDialog(title: title, context: contentContext, isPresented: isPresented, buttonViews: buttonViews, message: messageText) } } } @@ -259,7 +259,7 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { } } -@Composable func ComposeConfirmationDialog(title: Text?, context: ComposeContext, isPresented: Binding, actionViews: [View], message: Text?) { +@Composable func ComposeConfirmationDialog(title: Text?, context: ComposeContext, isPresented: Binding, buttonViews: [ButtonRepresentable], message: Text?) { let padding = 16.dp if let title { androidx.compose.material3.Text(modifier: Modifier.padding(horizontal: padding, vertical: 8.dp), color: Color.secondary.colorImpl(), text: title.localizedTextString(), style: Font.callout.bold().fontImpl()) @@ -274,48 +274,29 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { let buttonModifier = Modifier.padding(horizontal: padding, vertical: padding) let buttonFont = Font.title3 let tint = (EnvironmentValues.shared._tint ?? Color.accentColor).colorImpl() - guard !actionViews.isEmpty else { + guard !buttonViews.isEmpty else { ConfirmationDialogButton(action: { isPresented.set(false) }) { androidx.compose.material3.Text(modifier: buttonModifier, color: tint, text: stringResource(android.R.string.ok), style: buttonFont.fontImpl()) } return } - var cancelButton: Button? = nil - for actionView in actionViews { - var button = actionView as? Button - if let link = actionView as? Link { - link.ComposeAction() - button = link.content + var cancelButton: ButtonRepresentable? = nil + for buttonView in buttonViews { + guard buttonView.role != .cancel else { + cancelButton = buttonView + continue } - if let button { - guard button.role != .cancel else { - cancelButton = button - continue - } - ConfirmationDialogButton(action: { isPresented.set(false); button.action() }) { - let text = button.label.collectViews(context: context).compactMap { - $0.strippingModifiers { $0 as? Text } - }.first - let color = button.role == .destructive ? Color.red.colorImpl() : tint - androidx.compose.material3.Text(modifier: buttonModifier, color: color, text: text?.localizedTextString() ?? "", maxLines: 1, style: buttonFont.fontImpl()) - } - } else if let navigationLink = actionView as? NavigationLink { - let navigationAction = navigationLink.navigationAction() - ConfirmationDialogButton(action: { isPresented.set(false); navigationAction() }) { - let text = navigationLink.label.collectViews(context: context).compactMap { - $0.strippingModifiers { $0 as? Text } - }.first - androidx.compose.material3.Text(modifier: buttonModifier, color: tint, text: text?.localizedTextString() ?? "", maxLines: 1, style: buttonFont.fontImpl()) - } + ConfirmationDialogButton(action: { isPresented.set(false); buttonView.action() }) { + let text = buttonView.makeComposeLabel().textRepresentation(context: context) + let color = buttonView.role == .destructive ? Color.red.colorImpl() : tint + androidx.compose.material3.Text(modifier: buttonModifier, color: color, text: text?.localizedTextString() ?? "", maxLines: 1, style: buttonFont.fontImpl()) } androidx.compose.material3.Divider() } if let cancelButton { ConfirmationDialogButton(action: { isPresented.set(false); cancelButton.action() }) { - let text = cancelButton.label.collectViews(context: context).compactMap { - $0.strippingModifiers { $0 as? Text } - }.first + let text = cancelButton.makeComposeLabel().textRepresentation(context: context) androidx.compose.material3.Text(modifier: buttonModifier, color: tint, text: text?.localizedTextString() ?? "", maxLines: 1, style: buttonFont.bold().fontImpl()) } } else { @@ -346,8 +327,8 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { let textFields: [TextField] = actionViews.compactMap { $0.strippingModifiers { ($0 as? TextField) ?? ($0 as? SecureField)?.textField } } - let optionViews: [View] = actionViews.compactMap { - $0.strippingModifiers { $0 as? Button ?? $0 as? NavigationLink ?? $0 as? Link } + let buttonViews = actionViews.compactMap { + $0.strippingModifiers { $0 as? ButtonRepresentable } } let messageViews: [View] if let composeBuilder = message as? ComposeBuilder { @@ -358,21 +339,21 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { messageViews = [] } let messageText = messageViews.compactMap { - $0.strippingModifiers { $0 as? Text } - }.first + $0.strippingModifiers { $0 as? TextRepresentable } + }.first?.textRepresentation(context: context) BasicAlertDialog(onDismissRequest: { isPresented.set(false) }) { let modifier = Modifier.wrapContentWidth().wrapContentHeight().then(context.modifier) Surface(modifier: modifier, shape: MaterialTheme.shapes.large, tonalElevation: AlertDialogDefaults.TonalElevation) { let contentContext = context.content() Column(modifier: Modifier.padding(top: 16.dp, bottom: 4.dp), horizontalAlignment: androidx.compose.ui.Alignment.CenterHorizontally) { - ComposeAlert(title: title, titleResource: titleResource, context: contentContext, isPresented: isPresented, textFields: textFields, actionViews: optionViews, message: messageText) + ComposeAlert(title: title, titleResource: titleResource, context: contentContext, isPresented: isPresented, textFields: textFields, buttonViews: buttonViews, message: messageText) } } } } -@Composable func ComposeAlert(title: Text?, titleResource: Int? = nil, context: ComposeContext, isPresented: Binding, textFields: [TextField], actionViews: [View], message: Text?) { +@Composable func ComposeAlert(title: Text?, titleResource: Int? = nil, context: ComposeContext, isPresented: Binding, textFields: [TextField], buttonViews: [ButtonRepresentable], message: Text?) { let padding = 16.dp if let title { androidx.compose.material3.Text(modifier: Modifier.padding(horizontal: padding, vertical: 8.dp), color: Color.primary.colorImpl(), text: title.localizedTextString(), style: Font.title3.bold().fontImpl(), textAlign: TextAlign.Center) @@ -394,81 +375,60 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { let buttonModifier = Modifier.padding(horizontal: padding, vertical: 12.dp) let buttonFont = Font.title3 let tint = (EnvironmentValues.shared._tint ?? Color.accentColor).colorImpl() - guard !actionViews.isEmpty else { - AlertButton(modifier: Modifier.fillMaxWidth(), view: nil, isPresented: isPresented) { + guard !buttonViews.isEmpty else { + AlertButton(modifier: Modifier.fillMaxWidth(), buttonView: nil, isPresented: isPresented) { androidx.compose.material3.Text(modifier: buttonModifier, color: tint, text: stringResource(android.R.string.ok), style: buttonFont.fontImpl()) } return } - let buttonContent: @Composable (View, Bool) -> Void = { view, isCancel in - let button = view as? Button ?? (view as? Link)?.content - let label = button?.label ?? (view as? NavigationLink)?.label - let text = label?.collectViews(context: context).compactMap { - $0.strippingModifiers { $0 as? Text } - }.first - let color = button?.role == .destructive ? Color.red.colorImpl() : tint + let buttonContent: @Composable (ButtonRepresentable, Bool) -> Void = { buttonRep, isCancel in + let text = buttonRep.makeComposeLabel().textRepresentation(context: context) + let color = buttonRep.role == .destructive ? Color.red.colorImpl() : tint let style = isCancel ? buttonFont.bold().fontImpl() : buttonFont.fontImpl() androidx.compose.material3.Text(modifier: buttonModifier, color: color, text: text?.localizedTextString() ?? "", maxLines: 1, style: style) } - let optionViews = actionViews.filter { - if let button = $0 as? Button { - return button.role != .cancel - } - return $0 is Link || $0 is NavigationLink - } - let cancelButton = actionViews.first { - guard let button = $0 as? Button else { return false } - return button.role == .cancel - } + let buttonViewsExcludingCancel = buttonViews.filter { $0.role != .cancel } + let cancelButton = buttonViews.first { $0.role == .cancel } let cancelCount = cancelButton == nil ? 0 : 1 - if optionViews.count + cancelCount == 2 { + if buttonViewsExcludingCancel.count + cancelCount == 2 { // Horizontal layout for two buttons //TODO: Should revert to vertical when text is too long Row(modifier: Modifier.fillMaxWidth().height(IntrinsicSize.Min)) { let modifier = Modifier.weight(Float(1.0)) - if let view = cancelButton ?? optionViews.first { - AlertButton(modifier: modifier, view: view, isPresented: isPresented) { - buttonContent(view, view === cancelButton) + if let buttonRep = cancelButton ?? buttonViewsExcludingCancel.first { + AlertButton(modifier: modifier, buttonView: buttonRep, isPresented: isPresented) { + buttonContent(buttonRep, buttonRep === cancelButton) } androidx.compose.material3.VerticalDivider() } - if let button = optionViews.last { - AlertButton(modifier: modifier, view: button, isPresented: isPresented) { - buttonContent(button, false) + if let buttonRep = buttonViewsExcludingCancel.last { + AlertButton(modifier: modifier, buttonView: buttonRep, isPresented: isPresented) { + buttonContent(buttonRep, false) } } } } else { // Vertical layout let modifier = Modifier.fillMaxWidth() - for actionView in optionViews { - AlertButton(modifier: modifier, view: actionView, isPresented: isPresented) { - buttonContent(actionView, false) + for buttonView in buttonViewsExcludingCancel { + AlertButton(modifier: modifier, buttonView: buttonView, isPresented: isPresented) { + buttonContent(buttonView, false) } - if actionView !== optionViews.last || cancelButton != nil { + if buttonView !== buttonViewsExcludingCancel.last || cancelButton != nil { androidx.compose.material3.Divider() } } if let cancelButton { - AlertButton(modifier: modifier, view: cancelButton, isPresented: isPresented) { + AlertButton(modifier: modifier, buttonView: cancelButton, isPresented: isPresented) { buttonContent(cancelButton, true) } } } } -@Composable func AlertButton(modifier: Modifier, view: View?, isPresented: Binding, content: @Composable () -> Void) { - var action: (() -> Void)? - if let button = view as? Button { - action = button.action - } else if let link = view as? Link { - link.ComposeAction() - action = link.content.action - } else if let navigationLink = view as? NavigationLink { - action = navigationLink.navigationAction() - } - Box(modifier: modifier.clickable(onClick: { isPresented.set(false); action?() }), contentAlignment: androidx.compose.ui.Alignment.Center) { +@Composable func AlertButton(modifier: Modifier, buttonView: ButtonRepresentable?, isPresented: Binding, content: @Composable () -> Void) { + Box(modifier: modifier.clickable(onClick: { isPresented.set(false); buttonView?.action() }), contentAlignment: androidx.compose.ui.Alignment.Center) { content() } } diff --git a/Sources/SkipUI/SkipUI/Text/Label.swift b/Sources/SkipUI/SkipUI/Text/Label.swift index 74274b0..a3ae521 100644 --- a/Sources/SkipUI/SkipUI/Text/Label.swift +++ b/Sources/SkipUI/SkipUI/Text/Label.swift @@ -16,7 +16,7 @@ import androidx.compose.ui.unit.dp #endif // SKIP @bridge -public struct Label : View { +public struct Label : View, TextRepresentable { let title: ComposeBuilder let image: ComposeBuilder @@ -81,6 +81,12 @@ public struct Label : View { } } + @Composable func textRepresentation(context: ComposeContext) -> Text? { + return title.collectViews(context: context).compactMap { + $0.strippingModifiers { $0 as? Text } + }.first + } + @Composable private func ComposeLabel(context: ComposeContext, imageColor: Color?, titlePadding: Double) { Row(modifier: context.modifier, horizontalArrangement: Arrangement.spacedBy(8.dp), verticalAlignment: androidx.compose.ui.Alignment.CenterVertically) { ComposeImage(context: context.content(), imageColor: imageColor) diff --git a/Sources/SkipUI/SkipUI/Text/Text.swift b/Sources/SkipUI/SkipUI/Text/Text.swift index 4b2fe3d..e01e2e1 100644 --- a/Sources/SkipUI/SkipUI/Text/Text.swift +++ b/Sources/SkipUI/SkipUI/Text/Text.swift @@ -40,8 +40,14 @@ import skip.foundation.Locale import struct CoreGraphics.CGFloat #endif +protocol TextRepresentable { + #if SKIP + @Composable func textRepresentation(context: ComposeContext) -> Text? + #endif +} + // SKIP @bridge -public struct Text: View, Equatable { +public struct Text: View, Equatable, TextRepresentable { private let textView: _Text private let modifiedView: any View @@ -94,6 +100,10 @@ public struct Text: View, Equatable { } #if SKIP + @Composable func textRepresentation(context: ComposeContext) -> Text? { + return self + } + /// Interpret the key against the given bundle and the environment's current locale. @Composable public func localizedTextString() -> String { return textView.localizedTextString() @@ -305,7 +315,7 @@ public struct Text: View, Equatable { @available(*, unavailable) public static let offset = DateStyle(format: { _ in fatalError() }) - + @available(*, unavailable) public static let timer = DateStyle(format: { _ in fatalError() })