Skip to content

Instantly share code, notes, and snippets.

@ryanlintott
Last active April 5, 2022 22:51
Show Gist options
  • Save ryanlintott/890f70a36942888c1f161f7207099bf1 to your computer and use it in GitHub Desktop.
Save ryanlintott/890f70a36942888c1f161f7207099bf1 to your computer and use it in GitHub Desktop.
RotationMatchingOrientationViewModifier
//
// InfoDictionary.swift
// FrameUp
//
// Created by Ryan Lintott on 2021-05-11.
//
import SwiftUI
struct InfoDictionary {
static let supportedOrientations: Set<UIDeviceOrientation> = {
if let orientations = Bundle.main.infoDictionary?["UISupportedInterfaceOrientations"] as? [String] {
return Set(orientations.compactMap({ UIDeviceOrientation(key: $0) }))
} else {
return []
}
}()
}
//
// RotationMatchingOrientationViewModifier.swift
// FrameUp
//
// Created by Ryan Lintott on 2020-12-31.
//
import SwiftUI
public struct RotationMatchingOrientationViewModifier: ViewModifier {
@State private var contentOrientation: UIDeviceOrientation? = nil
@State private var deviceOrientation: UIDeviceOrientation? = nil
let isOn: Bool
let allowedOrientations: Set<UIDeviceOrientation>
let animation: Animation?
let screenOrientations: [UIDeviceOrientation] = [.portrait, .landscapeLeft, .landscapeRight, .portraitUpsideDown]
public init(isOn: Bool? = nil, allowedOrientations: Set<UIDeviceOrientation>? = nil, withAnimation animation: Animation? = nil) {
self.isOn = isOn ?? true
self.allowedOrientations = allowedOrientations ?? [.portrait, .landscapeLeft, .landscapeRight]
self.animation = animation
}
var rotation: Angle {
guard isOn else { return .zero }
switch (deviceOrientation, contentOrientation) {
case (.portrait, .landscapeLeft), (.landscapeLeft, .portraitUpsideDown), (.portraitUpsideDown, .landscapeRight), (.landscapeRight, .portrait):
return .degrees(90)
case (.portrait, .landscapeRight), (.landscapeRight, .portraitUpsideDown), (.portraitUpsideDown, .landscapeLeft), (.landscapeLeft, .portrait):
return .degrees(-90)
case (.portrait, .portraitUpsideDown), (.landscapeRight, .landscapeLeft), (.portraitUpsideDown, .portrait), (.landscapeLeft, .landscapeRight):
return .degrees(180)
default:
return .zero
}
}
var isLandscape: Bool {
switch (deviceOrientation, contentOrientation) {
case (.portrait, .landscapeLeft), (.portrait, .landscapeRight), (.portraitUpsideDown, .landscapeLeft), (.portraitUpsideDown, .landscapeRight):
return true
case (nil, _):
return !allowedOrientations.contains(.portrait)
default:
return false
}
}
func changeContentOrientation() {
if allowedOrientations.contains(UIDevice.current.orientation) {
contentOrientation = UIDevice.current.orientation
}
if contentOrientation == nil {
contentOrientation = screenOrientations.first(where: { allowedOrientations.contains($0) }) ?? .portrait
}
}
func changeDeviceOrientation() {
// Might be .faceUp or .unknown or similar
let newOrientation = UIDevice.current.orientation
if deviceOrientation == newOrientation { return }
deviceOrientation = InfoDictionary.supportedOrientations.first(where: { $0 == newOrientation }) ?? screenOrientations.first(where: { InfoDictionary.supportedOrientations.contains($0) })
}
func changeOrientations() {
if isOn {
withAnimation(animation) {
changeDeviceOrientation()
changeContentOrientation()
print("Device: \(deviceOrientation?.string ?? "nil") Content: \(contentOrientation?.string ?? "nil")")
}
}
}
public func body(content: Content) -> some View {
GeometryReader { proxy in
content
.rotationEffect(rotation)
.frame(width: isLandscape ? proxy.size.height : proxy.size.width, height: isLandscape ? proxy.size.width : proxy.size.height)
.position(x: proxy.size.width / 2, y: proxy.size.height / 2)
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
changeOrientations()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
changeOrientations()
}
.onAppear {
changeOrientations()
}
}
}
extension View {
public func rotationMatchingOrientation(_ allowedOrientations: Set<UIDeviceOrientation>? = nil, isOn: Bool? = nil, withAnimation animation: Animation? = nil) -> some View {
self.modifier(RotationMatchingOrientationViewModifier(isOn: self is EmptyView ? false : isOn, allowedOrientations: allowedOrientations, withAnimation: animation))
}
}
//
// UIDeviceOrientation-extension.swift
// FrameUp
//
// Created by Ryan Lintott on 2021-05-14.
//
import SwiftUI
extension UIDeviceOrientation {
init?(key: String) {
switch key {
case "UIInterfaceOrientationPortrait":
self = .portrait
case "UIInterfaceOrientationLandscapeLeft":
/// UIInterfaceOrientationLandscapeLeft means the interface has turned to the LEFT even though the device has turned to the RIGHT.
self = .landscapeRight
case "UIInterfaceOrientationLandscapeRight":
/// UIInterfaceOrientationLandscapeLeft means the interface has turned to the RIGHT even though the device has turned to the LEFT.
self = .landscapeLeft
case "UIInterfaceOrientationPortraitUpsideDown":
self = .portraitUpsideDown
case "UIInterfaceOrientationUnknown":
self = .unknown
default:
return nil
}
}
var string: String {
switch self {
case .portrait:
return "portrait"
case .landscapeLeft:
return "landscapeLeft"
case .landscapeRight:
return "landscapeRight"
case .portraitUpsideDown:
return "portraitUpsideDown"
case .unknown:
return "unknown"
case .faceUp:
return "faceUp"
case .faceDown:
return "faceDown"
default:
return "*new case*"
}
}
}
@ryanlintott
Copy link
Author

This view modifier will rotate any SwiftUI view according to a supplied set of supported orientations, even if they aren't supported orientations at the app level. This is useful for when you have a portrait-only app and want a view to work in landscape, or if you have an app that supports all orientations and want to lock a view to portrait or landscape.

@ryanlintott
Copy link
Author

Important note: Animations between orientations are available but they don't work in all situations.

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