Skip to content

Instantly share code, notes, and snippets.

@simonbs
Created April 5, 2022 19:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simonbs/2bee78c9522d4599f978d270c2dd1335 to your computer and use it in GitHub Desktop.
Save simonbs/2bee78c9522d4599f978d270c2dd1335 to your computer and use it in GitHub Desktop.
Present a portrait-only UIViewController wrapped in UIViewControllerRepresentable from a SwiftUI view.

This code provides a solution to presenting a UIViewController wrapped in UIViewControllerRepresentable from a SwiftUI view with the view controller being locked to portrait orientation.

Be warned: The code involves swizzling and usage of private API.

/**
* This file shows how we can tie all of the above together.
*/
import SwiftUI
// This view can have any orientation-
struct SettingsView: View {
@State private var isPortraitOnlyViewPresented = false
var body: some View {
List {
Section {
Button {
// Immediately set the orientation to portrait. We could do this later but doing it
// as the very first thing when presenting our view controller looks best.
UIDevice.current.setInterfaceOrientation(.portrait)
isPortraitOnlyViewPresented = true
}
} label: {
Text("Present Portrait-Only View")
}
}
}.fullScreenCover(isPresented: $isPortraitOnlyViewPresented) {
ExamplePortraitOnlyView()
}
}
// Bridges ExamplePortraitOnlyViewController to SwiftUI. There's nothing special going on here.
struct ExamplePortraitOnlyView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
return ExamplePortraitOnlyViewController()
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
// Our view controller that can only be presented in portrait.
final class ExamplePortraitOnlyViewController: UIViewController {
// As soon as we have a parent view controller, i.e. the hosting controller, we'll set the forced presentation.
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
parent?.sbs_forcedSupportedInterfaceOrientations = .portrait
}
}
/**
* We use private API to set the interface orientation. Setting the interface orientation will immediately
* perform an animation that rotates our app to the specified orientation.
* This should be called immediately before presenting our view controller.
*/
extension UIDevice {
func setInterfaceOrientation(_ orientation: UIInterfaceOrientation) {
let selectorName = "orientation"
if self.orientation.rawValue != orientation.rawValue && responds(to: NSSelectorFromString(selectorName)) {
setValue(orientation.rawValue, forKey: selectorName)
}
}
}
/**
* Add the sbs_forcedSupportedInterfaceOrientations property to UIViewController. We'll swizzle UIViewController to return
* the value of sbs_forcedSupportedInterfaceOrientations in supportedInterfaceOrientations when it's set to anything but nil.
* Setting this value disables rotation in the view controller but does not guarantee that the view controller is presented
* with the required orientation. We'll enforce this immediately before presenting the view controller.
*/
import UIKit
private var forcedSupportedInterfaceOrientationsKey: Void?
extension UIViewController {
var sbs_forcedSupportedInterfaceOrientations: UIInterfaceOrientationMask? {
get {
if let boxedValue = objc_getAssociatedObject(self, &forcedSupportedInterfaceOrientationsKey) as? NSNumber {
return UIInterfaceOrientationMask(rawValue: boxedValue.uintValue)
} else {
return nil
}
}
set {
if let newValue = newValue {
let number = NSNumber(value: newValue.rawValue)
objc_setAssociatedObject(self, &forcedSupportedInterfaceOrientationsKey, number, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
} else {
objc_setAssociatedObject(self, &forcedSupportedInterfaceOrientationsKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
}
// Swizzle UIViewController to use our sbs_supportedInterfaceOrientations.
extension UIViewController {
// Make sure to call this early in the app's lifecycle.
static let classInit_supportedInterfaceOrientations: Void = {
if let originalMethod = class_getInstanceMethod(UIViewController.self, #selector(getter: supportedInterfaceOrientations)),
let swizzledMethod = class_getInstanceMethod(UIViewController.self, #selector(getter: sbs_supportedInterfaceOrientations)) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}()
@objc var sbs_supportedInterfaceOrientations: UIInterfaceOrientationMask {
if let forcedInterfaceOrientation = sbs_forcedSupportedInterfaceOrientations {
return forcedInterfaceOrientation
} else {
return self.sbs_supportedInterfaceOrientations
}
}
}
@simonbs
Copy link
Author

simonbs commented Apr 5, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment