Skip to content
Closed
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
4 changes: 2 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 81 additions & 0 deletions Sources/SwiftUIBackports/Internal/BackportFallbackMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import SwiftUI
import SwiftBackports

internal enum BackportFallbackMode {
case auto // automatically fallback to native SwiftUI SDK
case disable // disable fallback to native SwiftUI SDK

var allowFallback: Bool {
switch self {
case .auto:
return true
case .disable:
return false
}
}
}

private struct EnforceBackportEnvironmentKey: EnvironmentKey {
static var defaultValue: BackportFallbackMode = .auto
}

internal extension EnvironmentValues {
/// Controls how backported SwiftUI APIs behave within this view hierarchy.
///
/// This environment value determines whether compatible, native SwiftUI
/// implementations provided by the running OS should be used automatically,
/// or whether the backported implementations should be preferred exclusively.
///
/// Behavior:
/// - `.auto` (default): When the current OS version provides a native SwiftUI
/// API compatible with the backported functionality, the native API is used.
/// Otherwise, the backported implementation is used.
/// - `.disable`: Native SwiftUI fallbacks are not used, even if available.
/// The backported implementation remains active.
///
/// Use this setting to ensure consistent behavior across OS versions or to
/// explicitly validate and compare backported behavior against native APIs.
///
/// Example:
/// ```swift
/// struct RootView: View {
/// var body: some View {
/// ContentView()
/// .environment(\.backportFallbackMode, .auto) // Prefer native when available
/// // .environment(\.backportFallbackMode, .disable) // Force backports only
/// }
/// }
/// ```
var backportFallbackMode: BackportFallbackMode {
get { self[EnforceBackportEnvironmentKey.self] }
set { self[EnforceBackportEnvironmentKey.self] = newValue }
}
}

internal extension View {
/// Sets the backport fallback behavior for this view hierarchy.
///
/// Use this modifier to control whether backported SwiftUI APIs should
/// automatically fall back to the native SwiftUI implementations provided
/// by the running OS, or remain disabled and use only the backported
/// implementations.
///
/// - Parameter mode: The desired fallback mode:
/// - `.auto`: Prefer the built-in SwiftUI API when available on the
/// current platform version, and use the backport otherwise.
/// - `.disable`: Do not fall back to the built-in SwiftUI API; always
/// use the backported implementation where applicable.
///
/// - Returns: A view that applies the specified backport fallback mode to
/// itself and its descendants via the environment.
///
/// - Note: This setting is propagated through the environment using
/// `EnvironmentValues.backportFallbackMode`. Apply this modifier near the
/// root of a view subtree to ensure consistent behavior throughout that
/// subtree.
///
/// - SeeAlso: `EnvironmentValues.backportFallbackMode`, `BackportFallbackMode`
func backportFallbackMode(_ mode: BackportFallbackMode) -> some View {
environment(\.backportFallbackMode, mode)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ extension Backport<Any> {
let description: Description
let actions: Actions

@Environment(\.backportFallbackMode) private var fallbackMode

public init(
@ViewBuilder label: () -> Label,
@ViewBuilder description: () -> Description = { EmptyView() },
Expand All @@ -23,11 +25,21 @@ extension Backport<Any> {

public var body: some View {
SwiftUI.Group {
if #available(macOS 14.0, *), fallbackMode.allowFallback {
SwiftUI.ContentUnavailableView {
label
} description: {
description
} actions: {
actions
}
} else {
#if os(iOS)
iOS()
iOS()
#else
macOS()
macOS()
#endif
}
}
}

Expand All @@ -46,8 +58,9 @@ extension Backport<Any> {
actions
}
}
.padding()
.frame(minWidth: 400)
.padding(.horizontal, 5)
.padding(.vertical, 40)
.frame(maxWidth: 400)
}

private func iOS() -> some View {
Expand Down Expand Up @@ -145,6 +158,7 @@ private struct ContentUnavailableLabelStyle: BackportLabelStyle {
configuration.title
.foregroundColor(.secondary)
.font(.largeTitle.weight(.bold))
.multilineTextAlignment(.center)
}
}
}
Expand All @@ -167,6 +181,21 @@ private extension View {

#Preview {
VStack {
Text("Disabled Fallback (enforced back-port)")
Backport.ContentUnavailableView {
Backport.Label("Backport", systemImage: "star")
} description: {
Text("A description for the placeholder")
} actions: {
Button("Primary") { }
Button("Secondary") { }
}
.backportFallbackMode(.disable)
.background(Color.gray.opacity(0.3))

Divider()

Text("Automatic Fallback to Native")
Backport.ContentUnavailableView {
Backport.Label("Backport", systemImage: "star")
} description: {
Expand All @@ -176,21 +205,21 @@ private extension View {
Button("Secondary") { }
}
.background(Color.gray.opacity(0.3))
.padding()

Divider()

if #available(iOS 17, tvOS 17, macOS 14, watchOS 10, *) {
Text("Native SwiftUI")
ContentUnavailableView {
Label("Native", systemImage: "star")
Label("This is a lengthy string", systemImage: "star")
} description: {
Text("A description for the placeholder")
} actions: {
Button("Primary") { }
Button("Secondary") { }
}
.background(Color.gray.opacity(0.3))
.padding()
}
}
.frame(width: 200, height: 700)
}
74 changes: 69 additions & 5 deletions Sources/SwiftUIBackports/Shared/Label/Label.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,36 @@ extension Backport where Wrapped == Any {
/// }
///
public struct Label<Title, Icon>: View where Title: View, Icon: View {
let title: Title?
let icon: Icon?

@Environment(\.self) private var environment
@Environment(\.backportLabelStyle) private var style
@Environment(\.backportLabelStyle) private var backportStyle
@Environment(\.backportFallbackMode) private var fallbackMode
private var config: Backport<Any>.LabelStyleConfiguration

/// Creates a label with a custom title and icon.
public init(@ViewBuilder title: () -> Title, @ViewBuilder icon: () -> Icon) {
public init(
@ViewBuilder title: () -> Title,
@ViewBuilder icon: () -> Icon
) {
self.title = title()
self.icon = icon()
config = .init(title: .init(title()), icon: .init(icon()))
}

@MainActor public var body: some View {
style.makeBody(configuration: config.environment(environment))
SwiftUI.Group {
if #available(macOS 14.0, *), fallbackMode.allowFallback {
SwiftUI.Label {
title
} icon: {
icon
}
} else {
backportStyle.makeBody(configuration: config.environment(environment))
}
}
}
}

Expand Down Expand Up @@ -140,7 +158,7 @@ extension Backport.Label where Wrapped == Any, Title == Text, Icon == Image {
if #available(macOS 11, *) {
self.init(title: { Text(titleKey) }, icon: { Image(systemName: name) })
} else {
self.init(title: { Text(titleKey) }, icon: { Image("SFSymbold not supported on macOS 11") })
self.init(title: { Text(titleKey) }, icon: { Image("SF Symbole not supported on macOS 11") })
}
#else
self.init(title: { Text(titleKey) }, icon: { Image(systemName: name) })
Expand All @@ -158,7 +176,7 @@ extension Backport.Label where Wrapped == Any, Title == Text, Icon == Image {
if #available(macOS 11, *) {
self.init(title: { Text(title) }, icon: { Image(systemName: name) })
} else {
self.init(title: { Text(title) }, icon: { Image("SFSymbold not supported on macOS 11") })
self.init(title: { Text(title) }, icon: { Image("SF Symbole not supported on macOS 11") })
}
#else
self.init(title: { Text(title) }, icon: { Image(systemName: name) })
Expand Down Expand Up @@ -190,6 +208,52 @@ extension Backport.Label where Wrapped == Any {
/// - Parameter configuration: The label style to use.
public init(_ configuration: Backport.LabelStyleConfiguration) {
self.config = configuration
self.title = configuration.title as? Title
self.icon = configuration.icon as? Icon
}

}

#Preview {
VStack {
Text("Disabled Fallback (enforced back-port)")
Backport.Label {
Text("This is a lengthy string")
} icon: {
if #available(macOS 11.0, *) {
Image(systemName: "star")
} else {
// Fallback on earlier versions
}
}
.background(Color.gray.opacity(0.3))
.backportFallbackMode(.disable)

Divider()

Text("Automatic Fallback to Native")
Backport.Label {
Text("This is a lengthy string")
} icon: {
if #available(macOS 11.0, *) {
Image(systemName: "star")
} else {
// Fallback on earlier versions
}
}
.background(Color.gray.opacity(0.3))

Divider()

if #available(macOS 11.0, *) {
Text("Native SwiftUI")
Label {
Text("This is a lengthy string")
} icon: {
Image(systemName: "star")
}
.background(Color.gray.opacity(0.3))
}
}
.frame(width: 200, height: 200)
}