A lightweight SwiftUI package that provides clean, composable conditional modifier APIs for handling OS availability checks.
- Purpose
- Do I Need Conditionals? Quick Decision Guide
- When to Use Conditionals vs
#ifDirectives - Installation
- API Overview
- Usage Examples
- View Identity & Performance
- Why Conditionals?
- License
Important: This package is designed primarily for static conditions (OS versions, compile-time checks). Using conditionals with runtime state that changes frequently can cause view identity loss and state resets. See the View Identity & Performance section for details.
Conditionals simplifies applying SwiftUI modifiers based on:
- OS version availability (primary use case)
- Compile-time
#availablechecks - Static styling modifiers that don't change at runtime
Instead of cluttering your views with nested #available checks, Conditionals provides a fluent, chainable API that keeps your code clean and readable.
Best suited for: OS version checks, compile-time features, static configuration.
Not recommended for: Runtime state, collections, toggleable properties (use if/overlay/background instead).
Ask: "Will this condition change while my app is running?"
- NO (OS version, device type, platform) → ✅ Use Conditionals
- YES (state variables, user input, collections) → ❌ Use
if/ternary/overlay
// ✅ GOOD - OS version is static
.conditional(if: OSVersion.iOS(17)) { view in
view.fontDesign(.rounded)
}
// ❌ BAD - isActive changes at runtime
.conditional(if: isActive) { view in // Don't do this!
view.foregroundStyle(.blue)
}
// ✅ GOOD - Use ternary instead
.foregroundStyle(isActive ? .blue : .gray)💡 Remember: Conditionals = compile-time or launch-time decisions. Not runtime state changes!
See View Identity & Performance for detailed examples and technical explanation.
For platform-specific APIs that simply don't exist on certain platforms, use Swift's built-in compiler directives directly:
// ✅ Use #if os() - navigationBarTitleDisplayMode is iOS-only
NavigationStack {
ContentView()
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}Why not Conditionals? The API doesn't exist on macOS at all—it's not a version issue, it's a platform issue. Standard #if directives handle this cleanly at compile time.
Use Conditionals when you need to check OS versions for features that were introduced in specific releases:
// ✅ Use Conditionals - glassEffect requires iOS 26.0+
Text("Card")
.conditional { view in
if #available(iOS 26.0, *) {
view.glassEffect(.regular, in: .rect(cornerRadius: 12))
} else {
view.background(.regularMaterial, in: .rect(cornerRadius: 12))
}
}Why Conditionals? The API exists across platforms but requires a minimum OS version. Conditionals provides a clean, chainable API for these version-gated features.
| Scenario | Use | Example |
|---|---|---|
| Platform-specific API | #if os(iOS) |
.navigationBarTitleDisplayMode() (iOS only) |
| Version-gated feature | Conditionals | .glassEffect() (iOS 26.0+) |
| Platform + version | Both | #if os(iOS) with if #available(iOS 26.0, *) |
Add Conditionals to your project via Xcode:
- File → Add Package Dependencies
- Enter the repository URL
- Select version/branch
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/aeastr/Conditionals.git", from: "1.0.0")
]- iOS 16.0+
- visionOS 1.0+
- macOS 13.0+
- watchOS 9.0+
- tvOS 16.0+
Apply a modifier only when a condition is true.
Text("Hello")
.conditional(if: OSVersion.iOS(26)) { view in
view.fontDesign(.rounded)
}Apply one modifier when true, another when false.
Text("Adaptive")
.conditional { view in
if #available(iOS 26.0, *) {
view.glassEffect(.regular, in: .rect(cornerRadius: 12))
} else {
view.background(.regularMaterial, in: .rect(cornerRadius: 12))
}
}Full control over availability checks within the closure.
Text("Custom Check")
.conditional { view in
if #available(iOS 26.0, *) {
view.glassEffect(.regular, in: .rect(cornerRadius: 12))
} else if #available(iOS 18.0, *) {
view.background(.regularMaterial, in: .rect(cornerRadius: 12))
} else {
view.background(Color.gray.opacity(0.3))
}
}Apply a modifier when an optional value is non-nil, with the unwrapped value available.
Text("Hello")
.conditional(if: optionalColor) { view, color in
view.foregroundStyle(color)
}Apply a modifier only when a condition is false. More readable than if: !condition.
Text("Content")
.conditional(unless: isCompact) { view in
view.padding(.horizontal, 40)
}All View conditional methods are available for ToolbarContent as well:
.conditional(if:apply:)- Basic conditional.conditional(if:apply:otherwise:)- With fallback.conditional(apply:)- Closure variant.conditional(if:apply:)- Optional unwrapping.conditional(unless:apply:)- Negated variant
ToolbarItem(placement: .topBarTrailing) {
Button("Action") {}
.conditional(if: OSVersion.iOS(26)) { view in
view.tint(.blue)
}
}Choose toolbar item placement conditionally using the Conditional protocol.
Select placement based on a boolean condition.
ToolbarItemGroup(
placement: .conditional(
if: OSVersion.iOS(26),
then: .bottomBar,
else: .secondaryAction
)
) {
Button("Edit") {}
Button("Share") {}
}Full control with closure for complex availability checks.
ToolbarItemGroup(
placement: .conditional {
#if os(iOS)
if #available(iOS 26.0, *) {
.bottomBar
} else {
.secondaryAction
}
#else
.automatic
#endif
}
) {
Button("Edit") {}
}Select placement based on a negated condition.
ToolbarItemGroup(
placement: .conditional(
unless: isCompact,
then: .principal,
else: .automatic
)
) {
Text("Title")
}For types that don't have specific extensions, use the generic conditional() functions:
Select any value conditionally.
let color = conditional(
if: isDarkMode,
then: Color.white,
else: Color.black
)
Text("Hello")
.foregroundStyle(
conditional(
if: OSVersion.iOS(17),
then: Color.red.gradient,
else: Color.red
)
)Full control for #available checks with any type.
.presentationDetents([
conditional {
if #available(iOS 16.0, *) {
PresentationDetent.height(300)
} else {
.medium
}
}
])Select values based on a negated condition.
let padding = conditional(
unless: isCompact,
then: 40.0,
else: 16.0
)Transform optional values or provide fallback.
let spacing = conditional(
if: customSpacing,
transform: { $0 * 2 },
else: 16.0
)You can make any type work with .conditional() syntax by conforming to the Conditional protocol. It's as simple as one line:
extension PresentationDetent: Conditional {}That's it! No implementation needed. Now you get all conditional methods for free:
// Value selection with dot syntax
.presentationDetents([
.conditional(
if: OSVersion.iOS(16),
then: .height(300),
else: .medium
)
])
// Or with #available checks
.presentationDetents([
.conditional {
if #available(iOS 16.0, *) {
.height(300)
} else {
.medium
}
}
])
// Or with unless variant
.presentationDetents([
.conditional(
unless: isCompact,
then: .large,
else: .medium
)
])// Make ScrollBounceBehavior conditional
extension ScrollBounceBehavior: Conditional {}
ScrollView {
content
}
.scrollBounceBehavior(
.conditional(
if: OSVersion.iOS(18),
then: .basedOnSize,
else: .always
)
)
// Make any custom type conditional
extension MyCustomPlacement: Conditional {}
myView.customModifier(
placement: .conditional(
if: someCondition,
then: .leading,
else: .trailing
)
)The protocol provides both static value selection methods and instance transformation methods, so conforming types get the full conditional API automatically.
Check specific OS versions at runtime.
OSVersion.iOS(18) // Check iOS 18+
OSVersion.macOS(14) // Check macOS 14+
OSVersion.watchOS(10) // Check watchOS 10+
OSVersion.tvOS(17) // Check tvOS 17+
OSVersion.supportsGlassEffect // Convenience: iOS 26+Quick boolean checks for specific versions.
OS.is26 // true if iOS 26+
let style = OS.is26 ? .glass : .regularText("Hello")
.conditional(if: someCondition) { view in
view.padding(20)
}Text("Modern UI")
.conditional { view in
if #available(iOS 26.0, *) {
view.glassEffect(.regular, in: .rect(cornerRadius: 12))
} else {
view.background(.regularMaterial, in: .rect(cornerRadius: 12))
}
}For features that require compile-time availability (like iOS 26's glass effects), use the closure variant:
Text("Glass Effect")
.conditional { view in
if #available(iOS 26.0, *) {
view.glassEffect(.regular, in: .rect(cornerRadius: 16))
} else {
view.background(.ultraThinMaterial, in: .rect(cornerRadius: 16))
}
}💡 Note: If you're specifically working with iOS 26's glass effects and want a dedicated solution, check out UniversalGlass. It brings SwiftUI's iOS 26 glass APIs to earlier deployments with lightweight shims—keeping your UI consistent on iOS 18+, while automatically deferring to real implementations wherever they exist.
Text("Adaptive Card")
.padding()
.conditional { view in
if #available(iOS 26.0, *) {
view.glassEffect(.regular, in: .rect(cornerRadius: 12))
} else {
view.background(.regularMaterial, in: .rect(cornerRadius: 12))
}
}Text("Cross-Platform")
.conditional { view in
#if os(iOS)
if #available(iOS 26.0, *) {
view.glassEffect(.regular, in: .rect(cornerRadius: 12))
} else if #available(iOS 18.0, *) {
view.background(.regularMaterial, in: .rect(cornerRadius: 12))
} else {
view.background(Color.gray.opacity(0.3))
}
#elseif os(macOS)
view.background(Color(.windowBackgroundColor))
#else
view
#endif
}NavigationStack {
ContentView()
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Action") {}
.conditional(if: OSVersion.iOS(26)) { view in
view.tint(.blue)
}
}
}
}struct ContentView: View {
let isCompact: Bool
var body: some View {
Text("Article")
.conditional(unless: isCompact) { view in
view
.frame(maxWidth: 600)
.padding(.horizontal, 40)
}
}
}For platform-specific code, use Swift's built-in #if os() compiler directives:
Text("Cross-Platform")
.conditional { view in
#if os(iOS)
view.padding()
.background(.blue.opacity(0.2))
#elseif os(macOS)
view.padding()
.background(.green.opacity(0.2))
#else
view
#endif
}When you conditional wrap an entire view based on runtime state that can change, SwiftUI sees it as a completely different view when the condition toggles. This causes:
- ❌ Full view reconstruction - SwiftUI tears down and rebuilds the entire view hierarchy
- ❌ Lost state - any
@Stateinside gets reset - ❌ Broken animations - transitions get interrupted
- ❌ Performance hit - unnecessary layout passes
struct BadExample: View {
@State private var items: [String] = []
@State private var counter = 0 // This will RESET!
var body: some View {
Text("Taps: \(counter)")
.onTapGesture { counter += 1 }
// ❌ BAD: When items changes from empty → non-empty,
// SwiftUI sees this as a completely different view tree
.conditional(if: !items.isEmpty) { view in
view.badge(items.count)
}
}
}What happens: When items goes from empty → non-empty (or vice versa), SwiftUI sees two completely different view types:
- Empty state:
Textwith no badge - Non-empty state:
Textwith badge modifier
The view identity changes, so SwiftUI destroys the old view and creates a new one. Your counter state gets reset to 0.
struct GoodExample: View {
@State private var items: [String] = []
@State private var counter = 0 // This is PRESERVED!
var body: some View {
Text("Taps: \(counter)")
.onTapGesture { counter += 1 }
// ✅ GOOD: Use overlay - the Text maintains its identity
.overlay(alignment: .topTrailing) {
if !items.isEmpty {
Badge(count: items.count)
}
}
}
}What happens: The Text view always has the same identity. Only the overlay appears/disappears. SwiftUI's diffing algorithm can efficiently update just the overlay without touching the Text or its state.
Conditionals should be inside view builders (
overlay,background,ifstatements inbody), not wrapping the view itself when the condition can change at runtime.
These conditions never change at runtime, so view identity is stable:
// ✅ Safe - OS version never changes at runtime
Text("Hello")
.conditional(if: OSVersion.iOS(26)) { view in
view.fontDesign(.rounded)
}
// ✅ Safe - Styling modifiers don't affect view identity
Text("Content")
.conditional(if: isDarkMode) { view in
view.foregroundStyle(.white)
}Modifiers that only affect appearance, not structure:
// ✅ Colors, fonts, padding - all safe
Text("Title")
.conditional(if: isHighlighted) { view in
view
.foregroundStyle(.blue)
.fontWeight(.bold)
.padding()
}
// ✅ Effects are safe when the condition is for styling
Text("Card")
.conditional { view in
if #available(iOS 26.0, *) {
view.glassEffect(.regular, in: .rect(cornerRadius: 12))
} else {
view.background(.regularMaterial, in: .rect(cornerRadius: 12))
}
}Avoid wrapping views when the condition depends on app state that can change:
// ❌ BAD: Collection state can change
Text("Items")
.conditional(if: !items.isEmpty) { view in
view.badge(items.count)
}
// ✅ GOOD: Use overlay instead
Text("Items")
.overlay {
if !items.isEmpty {
Badge(count: items.count)
}
}
// ❌ BAD: Boolean state can toggle
Text("Status")
.conditional(if: isActive) { view in
view.background(.green)
}
// ✅ GOOD: Put conditional inside background
Text("Status")
.background {
if isActive {
Color.green
}
}| Use Case | Safe? | Recommendation |
|---|---|---|
| OS version checks | ✅ Yes | Use conditionals - version never changes |
| Styling (colors, fonts) | ✅ Yes | Use conditionals - doesn't affect identity |
| Static padding/spacing | ✅ Yes | Use conditionals if condition is static |
| Platform detection | Use #if os(iOS) instead - more idiomatic |
|
| Collection-based badges | ❌ No | Use overlay { if !items.isEmpty { ... } } |
| State-dependent structure | ❌ No | Use if statements in view body |
| Animated content | ❌ No | Keep conditionals inside view builders |
| Complex view hierarchies | ❌ No | Wrap individual modifiers, not whole trees |
Ask yourself: "Can this condition change while the app is running?"
- No (OS version, platform, device type) → ✅ Safe to use conditionals
- Yes (app state, user preferences, collections) → ❌ Use
if/overlay/backgroundinstead
Conditionals shines when you need to handle multiple OS versions. Compare the verbose before to the clean after:
Group {
if #available(iOS 18.0, *) {
Text("Hello")
.fontWeight(.semibold)
.background(.ultraThinMaterial, in: .rect(cornerRadius: 12))
} else {
if #available(iOS 17.0, *) {
Text("Hello")
.fontWeight(.regular)
.background(.regularMaterial, in: .rect(cornerRadius: 12))
} else {
Text("Hello")
.fontWeight(.regular)
.background(Color.gray.opacity(0.3), in: .rect(cornerRadius: 12))
}
}
}Text("Hello")
.conditional(if: OSVersion.iOS(26)) { view in
view.fontWeight(.semibold)
}
.conditional { view in
if #available(iOS 26.0, *) {
view.glassEffect(.regular, in: .rect(cornerRadius: 12))
} else if #available(iOS 18.0, *) {
view.background(.regularMaterial, in: .rect(cornerRadius: 12))
} else {
view.background(Color.gray.opacity(0.3), in: .rect(cornerRadius: 12))
}
}Clean, maintainable, and focused on what the package does best: OS version checking.
MIT License - see LICENSE file for details.