ivan-magda/swiftui-interface-orientation
Per-view orientation locking for SwiftUI apps. Attach one modifier to a view and it locks the app to the orientations you specify while that view is on screen.
Table of Contents
Background
SwiftUI has no built-in way to control orientation per view. Your options are to lock the whole app through Info.plist, or to manage application(_:supportedInterfaceOrientationsFor:) by hand and track which screen is visible across the UIKit lifecycle.
This package replaces that bookkeeping with a single view modifier. Each view registers its allowed orientations when it appears and removes them when it disappears. A shared InterfaceOrientationManager holds the registry, computes the intersection of every active constraint, and asks the system to re-evaluate the supported orientations. When two visible views ask for orientations that don't overlap, the manager falls back to your app defaults instead of returning an empty mask.
Features
- One modifier,
.supportedInterfaceOrientations(_:), applied directly to any view. - Constraints register on
onAppearand clear ononDisappear, so navigating away restores the previous orientation. - Multiple visible views resolve to the intersection of their masks; an empty intersection falls back to your defaults.
- Defaults read from
UISupportedInterfaceOrientationsinInfo.plist, or set them explicitly throughConfiguration. - Updates supported orientations through
setNeedsUpdateOfSupportedInterfaceOrientations()on iOS 16+, falling back toattemptRotationToDeviceOrientation()on earlier versions. @MainActor-isolated manager built for the Swift 6 language mode.
Installation
Xcode
- Open File → Add Package Dependencies…
- Enter the URL:
https://github.com/ivan-magda/swiftui-interface-orientation.git - Choose the
SwiftUIInterfaceOrientationlibrary and add it to your target.
Package.swift
Add the package to your dependencies:
dependencies: [
.package(
url: "https://github.com/ivan-magda/swiftui-interface-orientation.git",
from: "1.2.0"
)
]Then add the product to your target:
.target(
name: "YourTarget",
dependencies: [
.product(
name: "SwiftUIInterfaceOrientation",
package: "swiftui-interface-orientation"
)
]
)App-level setup
The manager resolves orientations, but iOS only asks for them through your app delegate. Implement application(_:supportedInterfaceOrientationsFor:) and return InterfaceOrientationManager.shared.supportedInterfaceOrientations. Without this hook the modifier has no effect.
import SwiftUI
import SwiftUIInterfaceOrientation
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
supportedInterfaceOrientationsFor window: UIWindow?
) -> UIInterfaceOrientationMask {
InterfaceOrientationManager.shared.supportedInterfaceOrientations
}
}
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}The manager reads its defaults from the UISupportedInterfaceOrientations key in your app's Info.plist. To set defaults in code instead, see Custom defaults.
Usage
Lock a single view
Apply the modifier to any view. The constraint applies while the view is visible and clears when it leaves the screen.
import SwiftUI
import SwiftUIInterfaceOrientation
struct PortraitOnlyView: View {
var body: some View {
Text("Locked to portrait")
.supportedInterfaceOrientations(.portrait)
}
}Navigation
Each destination declares its own orientations. The constraint registers when the destination appears and clears when you navigate back.
struct ContentView: View {
var body: some View {
NavigationStack { // NavigationView on iOS 14-15
List {
NavigationLink("Portrait screen") {
Text("Portrait only")
.supportedInterfaceOrientations(.portrait)
}
NavigationLink("Landscape screen") {
Text("Landscape only")
.supportedInterfaceOrientations(.landscape)
}
}
}
}
}Allow all but upside down
A common choice for iPhone apps:
ContentView()
.supportedInterfaceOrientations(.allButUpsideDown)Clear a constraint
Pass nil or an empty mask to remove this view's constraint and let the defaults apply.
SomeView()
.supportedInterfaceOrientations(isLocked ? .portrait : nil)Custom defaults
By default the manager reads UISupportedInterfaceOrientations from Info.plist. To supply defaults in code, call configure(configuration:) before anything accesses InterfaceOrientationManager.shared. The call asserts if the manager has already initialized, so place it at the start of your launch sequence, such as in application(_:didFinishLaunchingWithOptions:).
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
InterfaceOrientationManager.configure(
configuration: .init(defaultOrientations: .portrait)
)
return true
}
func application(
_ application: UIApplication,
supportedInterfaceOrientationsFor window: UIWindow?
) -> UIInterfaceOrientationMask {
InterfaceOrientationManager.shared.supportedInterfaceOrientations
}
}Configuration.fromInfoPlist() is the default and reproduces the automatic behavior.
Project Structure
Sources/SwiftUIInterfaceOrientation/
├── InterfaceOrientationManager.swift // Registry, resolution, and Configuration
├── View+InterfaceOrientation.swift // The .supportedInterfaceOrientations(_:) modifier
└── MainBundleInfo.swift // Reads UISupportedInterfaceOrientations from Info.plistContributing
Issues and pull requests are welcome. The package builds with swift build, and the test suite runs against an iOS simulator:
xcodebuild test \
-scheme SwiftUIInterfaceOrientation \
-destination "platform=iOS Simulator,OS=18.4,name=iPhone 16" \
-configuration DebugLint with swiftlint --strict before opening a pull request.
License
Released under the MIT License. See LICENSE for the full text.
Package Metadata
Repository: ivan-magda/swiftui-interface-orientation
Default branch: main
README: README.md