Last active
April 5, 2022 22:51
-
-
Save ryanlintott/890f70a36942888c1f161f7207099bf1 to your computer and use it in GitHub Desktop.
RotationMatchingOrientationViewModifier
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 [] | |
} | |
}() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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)) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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*" | |
} | |
} | |
} |
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
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.