-
Notifications
You must be signed in to change notification settings - Fork 0
Cross Platform
DesignFoundation is designed to work identically on iOS 18+, macOS 15+, and visionOS 2+. This page explains how that works, where the seams are, and when you need a platform guard in your own code.
Related pages: Getting-Started · Theming · Navigation · Overlays-and-Feedback
DFPlatformContext is an internal type that captures the current rendering environment — platform, size class, display scale, and appearance — and injects it alongside DFTheme when you apply .dfTheme(_:) or .dfThemePreset(_:).
You do not create DFPlatformContext directly. It is populated automatically.
@Environment(\.dfPlatformContext) private var platform
var body: some View {
if platform.variant == .compact {
CompactLayout()
} else {
ExpandedLayout()
}
}public enum DFPlatformVariant: Sendable {
case automatic // Let DF resolve the correct variant
case compact // iPhone / iPod touch / compact-width iPad split view
case expanded // iPad regular width / macOS
case immersive // visionOS full-space
}DesignFoundation resolves the correct variant from @Environment(\.horizontalSizeClass) and @Environment(\.sizeCategory) internally. You read the resolved result via platform.variant.
DFPlatformContext also carries colorScheme. For custom views that need to branch on appearance, prefer reading @Environment(\.dfTheme) with pre-resolved color tokens — they already reflect the current color scheme. Fall back to platform.colorScheme only when you need to make a non-token decision:
@Environment(\.dfTheme) private var theme
@Environment(\.dfPlatformContext) private var platform
var body: some View {
// Prefer this — tokens already resolve to the correct appearance
Text("Hello").foregroundStyle(theme.colors.textPrimary)
// Use this only for decisions tokens don't cover
Image(platform.colorScheme == .dark ? "hero-dark" : "hero-light")
}The following components work identically on iOS, macOS, and visionOS without any #if os() or #available guard in your call site:
| Component | Platform behavior |
|---|---|
DFSidebar |
Sidebar on macOS / iPad regular; sheet/drawer on iPhone compact |
DFTabBar |
Bottom tab bar on iPhone; compact tab strip on macOS |
dfNavigationBar |
Toolbar + back-button on macOS; large/inline title on iOS |
DFModal |
Center card on macOS; full/partial sheet on iOS |
DFSheet |
Bottom sheet on iOS; popover-style on macOS |
DFPopover |
Native popover with arrow on macOS; floating panel on iOS |
DFTooltip |
.help() on macOS; long-press label on iOS |
All style .glass variants |
Gated internally — falls back to .default on OS < 26 |
struct RootView: View {
@State private var selectedTab = "home"
var body: some View {
#if os(macOS)
NavigationSplitView {
List(items, selection: $selectedItem) { item in
Label(item.label, systemImage: item.icon)
}
} detail: {
detailView(for: selectedItem)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("New") { createNew() }
}
}
}
#else
TabView(selection: $selectedTab) {
ForEach(tabs) { tab in
tabContent(for: tab.id)
.tabItem { Label(tab.label, systemImage: tab.icon) }
.tag(tab.id)
}
}
.tint(Color.blue)
#endif
}
}struct RootView: View {
@State private var selection: String? = "home"
var body: some View {
DFSidebar(selection: $selection, sections: navSections) { id in
detailView(for: id)
.dfNavigationBar(title: title(for: id)) {
DFButton("New") { createNew() }
.dfButtonStyle(.tinted)
}
}
}
}DFPlatformContext handles component-level platform differences. Some app-level APIs don't have DF wrappers — those still need guards.
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.dfThemePreset(.slate)
}
#if os(macOS)
WindowGroup("Detail", id: "detail", for: String.self) { $id in
DetailView(id: id ?? "")
}
.defaultSize(width: 900, height: 700)
.windowStyle(.titleBar)
#endif
}
}// Never use NSWorkspace.shared.open() from a SwiftUI view
// Use the cross-platform environment action:
@Environment(\.openURL) private var openURL
openURL(url)// Multi-window on macOS
@Environment(\.openWindow) private var openWindow
#if os(macOS)
Button("Open Detail") {
openWindow(id: "detail", value: item.id)
}
#endifIf your code calls APIs that exist only on one platform, you still need guards:
// UIKit-only
#if canImport(UIKit)
UIApplication.shared.open(url)
#endif
// AppKit-only
#if canImport(AppKit)
NSWorkspace.shared.open(url)
#endifFor URL opening specifically, always use @Environment(\.openURL) in SwiftUI — it works on all three platforms and avoids the guard entirely.
import SwiftUI
import DesignFoundation
// Package.swift declares:
// .iOS(.v18), .macOS(.v15), .visionOS(.v2)
@main
struct CrossPlatformApp: App {
var body: some Scene {
WindowGroup {
AppRootView()
.dfThemePreset(.aurora)
.dfToast(queue: DFToastQueue.shared)
}
#if os(macOS)
Settings {
SettingsView()
.dfThemePreset(.aurora)
}
#endif
}
}
struct AppRootView: View {
@State private var selection: String? = "feed"
private let sections = [
DFSidebarSection(id: "main", title: "", items: [
DFSidebarItem(id: "feed", icon: "list.bullet", label: "Feed"),
DFSidebarItem(id: "explore", icon: "magnifyingglass", label: "Explore"),
DFSidebarItem(id: "messages", icon: "message.fill", label: "Messages", badge: .count(3)),
DFSidebarItem(id: "profile", icon: "person.fill", label: "Profile"),
]),
]
var body: some View {
DFSidebar(selection: $selection, sections: sections) { id in
destination(for: id)
.dfNavigationBar(title: navTitle(for: id))
}
}
@ViewBuilder
private func destination(for id: String) -> some View {
switch id {
case "feed": FeedView()
case "explore": ExploreView()
case "messages": MessagesView()
case "profile": ProfileView()
default: ContentUnavailableView("Select a section", systemImage: "sidebar.left")
}
}
private func navTitle(for id: String) -> String {
switch id {
case "feed": return "Feed"
case "explore": return "Explore"
case "messages": return "Messages"
case "profile": return "Profile"
default: return ""
}
}
}let package = Package(
name: "MyApp",
platforms: [
.iOS(.v18),
.macOS(.v15),
.visionOS(.v2),
],
// …
)DF's .glass style variants are gated internally with @available(iOS 26, macOS 26, *) checks. You only need explicit @available guards in your own code if you are calling iOS 26 / macOS 26 APIs directly outside of DF:
@available(iOS 26, macOS 26, *)
struct MyGlassPanel: View {
// Calls a SwiftUI API that only exists in iOS 26
var body: some View {
Text("Glass panel")
.glassEffect(.regular) // iOS 26-only SwiftUI API
}
}DF's .glass variant on DFCard, DFButton, etc., is NOT one of these cases — DF gates it internally and falls back gracefully. You do not need @available in your call site for DF components.
No DesignFoundation-specific Info.plist keys are required. Follow Apple's standard platform requirements for each target (e.g., UILaunchScreen for iOS, NSPrincipalClass for macOS AppKit apps).
For a single multi-platform target (recommended):
- In your Xcode project, select the target.
- Under General → Supported Destinations, add iOS, macOS, and visionOS.
- Set deployment targets to iOS 18, macOS 15, visionOS 2.
- Under Frameworks, Libraries, and Embedded Content, confirm DesignFoundation is linked.
See also: Getting-Started · Theming · Navigation · Overlays-and-Feedback · Changelog