I have this iOS app which trying to downgrade to support older iPhones with iOS version 11.0, but ran into few problems.
I have the below line of code in AppDelegate which shows error 'connectedScenes' is only available in iOS 13.0 or newer and 'UIWindowScene' is only available in iOS 13.0 or newer
var keyWindowDev = UIApplication.shared.connectedScenes.filter({$0.activationState == .foregroundActive}).map({$0 as? UIWindowScene}).compactMap({$0}).first?.windows.filter({$0.isKeyWindow}).first
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate,UIWindowSceneDelegate, UNUserNotificationCenterDelegate,MessagingDelegate {
var window: UIWindow?
//....
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
}
}
//Below is keyWindowDev usage in AppDelegate
extension UIApplication {
class func topViewController(base: UIViewController? = keyWindowDev?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
return base
}
}
And keyWindowDev is also used to set SVProgressHUD Container View around the app
class HomeVC: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
SVProgressHUD.setContainerView(keyWindowDev)
}
}
Please how to change the line of code in keyWindowDev to support iOS 11?
You should be able to use something like this:
var keyWindowDev: UIWindow? = {
if #available(iOS 13.0, *) {
return UIApplication.shared.connectedScenes
.filter { $0.activationState == .foregroundActive }
.compactMap { $0 as? UIWindowScene }.first?.windows
.first(where: \.isKeyWindow)
} else {
return UIApplication.shared.keyWindow
}
}()
I made some optimizations to your original code.
Related
I have a widget with a link. I'm implementing the widget URL in SceneDelegate. The problem is when I tap on the widget I get a link, it works. But when I call View Controller function in SceneDelegate, it does nothing. Well, it can print something, but it doesn't change anything. SceneDelegate:
// App launched
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
window.rootViewController = ViewController()
self.window = window
window.makeKeyAndVisible()
getURL(urlContext: connectionOptions.urlContexts)
}
// App opened from background
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
getURL(urlContext: URLContexts)
}
// Get URL
private func getURL(urlContext: Set<UIOpenURLContext>) {
guard let urlContexts = urlContext.first(where: { $0.url.scheme == "app-deeplink" }) else { return }
print(urlContexts.url)
print("success")
let viewController = ViewController()
viewController.cameFromWidget()
}
Function in ViewController that changes labels text:
func cameFromWidget() {
label.text = "Hello"
print(label.text)
}
WidgetExtension:
var body: some View {
Text(entry.date, style: .time)
.widgetURL(URL(string: "app-deeplink://app"))
}
So ViewController func just prints the text but doesn't change it when I call it from SceneDelegate.
My widget link is only in WidgetExtension and SceneDelegate, I didn't add it to infoPlist.
My question: Why does it work but does nothing? Maybe I should add it to some file?
Thank you so much!
I found out. To make some changes in your viewController you need to make it with rootViewController:
private func getURL(urlContext: Set<UIOpenURLContext>) {
guard let urlContexts = urlContext.first(where: { $0.url.scheme == "app-deeplink" }) else { return }
print(urlContexts.url)
print("success")
let rootViewController = window?.rootViewController as? ViewController
rootViewController?.cameFromWidget()
}
According to the iOS & iPadOS 16 Beta 3 Release Notes:- Attempting to set an orientation on UIDevice via setValue:forKey: isn’t supported and no longer works. Instead, they say use: preferredInterfaceOrientationForPresentation.
In my case, force view controller orientation is not working in iOS 16 beta either by using preferredInterfaceOrientationForPresentation or requestGeometryUpdate.
Previously, UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation") was working fine.
It works for me.
In AppDelegate,
var orientation: UIInterfaceOrientationMask = .portrait
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return orientation
}
In view controller,
(UIApplication.shared.delegate as? AppDelegate)?.orientation = .landscapeRight
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeRight))
UIApplication.navigationTopViewController()?.setNeedsUpdateOfSupportedInterfaceOrientations()
In Helper,
extension UIApplication {
class func navigationTopViewController() -> UIViewController? {
let nav = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController
return nav?.topViewController
}
}
My problem with my code below is that I'm trying to do it when closing a modal view and the view under it are not updated quick enough. If I put the requestGeometryUpdate on a separate button then when I close the view it work.
if #available(iOS 16.0, *) {
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait))
}
It works for me:
import Foundation
import UIKit
extension UIViewController {
func setDeviceOrientation(orientation: UIInterfaceOrientationMask) {
if #available(iOS 16.0, *) {
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientation))
} else {
UIDevice.current.setValue(orientation.toUIInterfaceOrientation.rawValue, forKey: "orientation")
}
}
}
extension UIInterfaceOrientationMask {
var toUIInterfaceOrientation: UIInterfaceOrientation {
switch self {
case .portrait:
return UIInterfaceOrientation.portrait
case .portraitUpsideDown:
return UIInterfaceOrientation.portraitUpsideDown
case .landscapeRight:
return UIInterfaceOrientation.landscapeRight
case .landscapeLeft:
return UIInterfaceOrientation.landscapeLeft
default:
return UIInterfaceOrientation.unknown
}
}
}
How to use it?
Just call it on your UIViewController:
setDeviceOrientation(orientation: .landscapeRight)
EDIT
More completed solution:
import UIKit
final class DeviceOrientation {
static let shared: DeviceOrientation = DeviceOrientation()
// MARK: - Private methods
private var windowScene: UIWindowScene? {
return UIApplication.shared.connectedScenes.first as? UIWindowScene
}
// MARK: - Public methods
func set(orientation: UIInterfaceOrientationMask) {
if #available(iOS 16.0, *) {
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientation))
} else {
UIDevice.current.setValue(orientation.toUIInterfaceOrientation.rawValue, forKey: "orientation")
}
}
var isLandscape: Bool {
if #available(iOS 16.0, *) {
return windowScene?.interfaceOrientation.isLandscape ?? false
}
return UIDevice.current.orientation.isLandscape
}
var isPortrait: Bool {
if #available(iOS 16.0, *) {
return windowScene?.interfaceOrientation.isPortrait ?? false
}
return UIDevice.current.orientation.isPortrait
}
var isFlat: Bool {
if #available(iOS 16.0, *) {
return false
}
return UIDevice.current.orientation.isFlat
}
}
extension UIInterfaceOrientationMask {
var toUIInterfaceOrientation: UIInterfaceOrientation {
switch self {
case .portrait:
return UIInterfaceOrientation.portrait
case .portraitUpsideDown:
return UIInterfaceOrientation.portraitUpsideDown
case .landscapeRight:
return UIInterfaceOrientation.landscapeRight
case .landscapeLeft:
return UIInterfaceOrientation.landscapeLeft
default:
return UIInterfaceOrientation.unknown
}
}
}
How to use it:
DeviceOrientation.shared.set(orientation: .portrait)
I noticed my issue seems like resolved by calling method below:
[UIViewController setNeedsUpdateOfSupportedInterface
You may give it a try.
I tried all above solutions seem they're not 100% percent working. After through this post
https://developer.apple.com/forums/thread/707735 i got the hint. Let's try this below code. It’s worked for me.
if #available(iOS 16.0, *) {
DispatchQueue.main.async {
UIViewController.attemptRotationToDeviceOrientation()
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientation)) { error in
print(error)
print(windowScene?.effectiveGeometry)
}
navigationController?.topViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
}
}
setValue:forKey is a method of old NSObject (NSKeyValueCoding). It's not official documented and supported by UIDevice class. Using it is considering using a private api. Apple can terminate it anytime they want.
Apple released new API which is replaced with setValue:forKey:"orientation".
Apple update
guard let windowScene = view.window?.windowScene else { return }
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .landscape)) { error in
// Handle denial of request.
}
But I am having problem about UIDevice.orientationDidChangeNotification , it is not working
Following Apple's documentation you need to
Requests an update to the window scene’s geometry using the specified geometry preferences object.
https://developer.apple.com/documentation/uikit/uiwindowscene/3975944-requestgeometryupdate/
So using the code in the example, you can change the way we set the orientation in our views using requestGeometryUpdate and using as well setNeedsUpdateOFSupportedInterface
public extension UIViewController {
func deviceOrientation(orientation: UIInterfaceOrientationMask) {
if #available(iOS 16.0, *) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
else { return }
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: orientation))
self.setNeedsUpdateOfSupportedInterfaceOrientations()
}
}
}
How can I hide the Title Bar in the new SwiftUI App Protocol?
Since the AppDelegate.swift and SceneDelegate.swift protocols are gone, I cant follow this documentation anymore:
https://developer.apple.com/documentation/uikit/mac_catalyst/removing_the_title_bar_in_your_mac_app_built_with_mac_catalyst
I can't implement this code:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
#if targetEnvironment(macCatalyst)
if let titlebar = windowScene.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = nil
}
#endif
}
}
Hope it's still possible with the new AppProtocol..
Thanks in advance
This is how to hide the titlebar:
struct ContentView: View {
var body: some View {
ZStack {
Text("Example UI")
}
.withHostingWindow { window in
#if targetEnvironment(macCatalyst)
if let titlebar = window?.windowScene?.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = nil
}
#endif
}
}
}
extension View {
fileprivate func withHostingWindow(_ callback: #escaping (UIWindow?) -> Void) -> some View {
self.background(HostingWindowFinder(callback: callback))
}
}
fileprivate struct HostingWindowFinder: UIViewRepresentable {
var callback: (UIWindow?) -> ()
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
On your Scene, set .windowStyle(_:) to HiddenTitleBarWindowStyle().
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.windowStyle(HiddenTitleBarWindowStyle())
}
}
EDIT: Ah crap. While this API is supposedly available for Mac Catalyst according to the online documentation, it looks like it’s not actually marked as such in frameworks so you can’t use it.
Im trying to use the 3D touch Quick Actions and I'm setting it up copying VEA Software code. In his sample code it works perfectly but when I try to add it to my app I get some unusual errors. I am new to coding and swift so please explain as much as possible. Thanks. Below I have the code which is in my app delegate.
This is where I'm getting the error (self.window?):
#available(iOS 9.0, *)
func handleShortcutItem(shortcutItem: UIApplicationShortcutItem) -> Bool
{
var handled = false
var window: UIWindow?
guard ShortcutIdentifier(fullType: shortcutItem.type) != nil else { return false }
guard let shortcutType = shortcutItem.type as String? else { return false }
switch (shortcutType)
{
case ShortcutIdentifier.First.type:
handled = true
var window = UIWindow?()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navVC = storyboard.instantiateViewControllerWithIdentifier("ProjectPage") as! UINavigationController
// Error on line below
self.window?.rootViewController?.presentViewController(navVC, animated: true, completion: nil)
break
case ShortcutIdentifier.Second.type:
handled = true
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navVC = storyboard.instantiateViewControllerWithIdentifier("ContactPageView") as! UINavigationController
// Error on line below
self.window?.rootViewController?.presentViewController(navVC, animated: true, completion: nil)
break
case ShortcutIdentifier.Third.type:
handled = true
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navVC = storyboard.instantiateViewControllerWithIdentifier("ViewController") as! UINavigationController
// Error on line below
self.window?.rootViewController?.presentViewController(navVC, animated: true, completion: nil)
break
default:
break
}
return handled
}
Xcode 12.0 Swift 5.0
At the moment you need to:
Remove the “Application Scene Manifest” from info.plist file;
Remove scene delegate class;
Remove scene related methods in AppDelegate class;
If missing, add the property var window: UIWindow? to your AppDelegate class.
Add some logic in func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?).
Example of implementation when you need to support iOS 12 and 13:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
configureInitialViewController()
return true
}
private func configureInitialViewController() {
let initialViewController: UIViewController
let storyboard = UIStoryboard(name: "Main", bundle: nil)
window = UIWindow()
if (condition) {
let mainViewController = storyboard.instantiateViewController(withIdentifier: "mainForm")
initialViewController = mainViewController
} else {
let loginViewController = storyboard.instantiateViewController(withIdentifier: "loginForm")
initialViewController = loginViewController
}
window?.rootViewController = initialViewController
window?.makeKeyAndVisible()
}
}
In iOS 13, Xcode 11, the sceneDelegate handles the functionality of UIScene and window. The window performs properly when used in the sceneDelegate.
When you are using Scene view then windows object is in the scene delegate. Copy window object and place it in the app delegate and it will work.
if your project already has SceneDelegate file ,then you need to use it insead of AppDelegate , like this way :
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let _ = (scene as? UIWindowScene) else { return }
let mainStoryBoard = UIStoryboard(name: "Main", bundle: nil)
let viewController = mainStoryBoard.instantiateViewController(withIdentifier: "setCountry")
window?.rootViewController = viewController
}
Sometimes when you have an issue in AppDelegate it can be solved with a quick Product -> Clean. Hope this helps.
This worked for me.
Add the following code inside SceneDelegate's first function's, func scene(...), if statement, if let windowScene = scene as? UIWindowScene {..Add Below Code Here..}.
Make sure to add import MediaPlayer at the top of the file.
let volumeView = MPVolumeView()
volumeView.alpha = 0.000001
window.addSubview(volumeView)
At the end your code should be:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
// MARK: Hide Volume HUD View
let volumeView = MPVolumeView()
volumeView.alpha = 0.000001
window.addSubview(volumeView)
}
}
first you need to specify which shortcut you need to use , cuz there are two types of shortcuts : dynamic and static and let say that you choose the static that can be done and added from the property list info then you have to handle the actions for each shortcut in the app delegate file in your project
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let mainTabController = storyboard.instantiateViewController(withIdentifier: "Tab_Bar") as? Tab_Bar
let loginViewController = storyboard.instantiateViewController(withIdentifier: "LoginVC") as? LoginVC
let doctor = storyboard.instantiateViewController(withIdentifier: "DrTabController") as! DrTabController
getData(key: "token") { (token) in
getData(key: "type") { (type) in
if token != "" && type != "" , type == "2" {
self.window?.rootViewController = mainTabController
if #available(iOS 13.0, *) {
self.window?.overrideUserInterfaceStyle = .light
} else {
self.window?.makeKeyAndVisible()
}
if shortcutItem.type == "appointMent" {
mainTabController?.selectedIndex = 1
} else if shortcutItem.type == "Chatting" {
mainTabController?.selectedIndex = 2
}
} else if token != "" && type != "" , type == "1"{
self.window?.rootViewController = doctor
if #available(iOS 13.0, *) {
self.window?.overrideUserInterfaceStyle = .light
} else {
self.window?.makeKeyAndVisible()
}
if shortcutItem.type == "appointMent" {
mainTabController?.selectedIndex = 1
} else if shortcutItem.type == "Chatting" {
mainTabController?.selectedIndex = 2
}
} else if token == "" && type == "" {
self.window?.rootViewController = loginViewController
if #available(iOS 13.0, *) {
self.window?.overrideUserInterfaceStyle = .light
} else {
self.window?.makeKeyAndVisible()
}
if shortcutItem.type == "appointMent" {
mainTabController?.selectedIndex = 1
} else if shortcutItem.type == "Chatting" {
mainTabController?.selectedIndex = 2
}
}
}
}
}
this will be work so fine for you but first you have to determine the rootViewController we can say like :
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
self.window?.rootViewController = mainTabController
if #available(iOS 13.0, *) {
self.window?.overrideUserInterfaceStyle = .light
} else {
self.window?.makeKeyAndVisible()
}
if shortcutItem.type == "appointMent" {
mainTabController?.selectedIndex = 1
} else if shortcutItem.type == "Chatting" {
mainTabController?.selectedIndex = 2
}
I am trying to rotate one view while all other views (5) are fixed to portrait. The reason is that in that one view I want the user to watch pictures which he saved before. I guess this is possible but so far I couldn't figure out how to achieve that. Can anyone help or give me a hint?
I am programming that in Swift running on iOS8
I'd recommend using supportedInterfaceOrientationsForWindow in your appDelegate to allow rotation only in that specific view controller, ex:
Swift 4/Swift 5
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
// Make sure the root controller has been set
// (won't initially be set when the app is launched)
if let navigationController = self.window?.rootViewController as? UINavigationController {
// If the visible view controller is the
// view controller you'd like to rotate, allow
// that window to support all orientations
if navigationController.visibleViewController is SpecificViewController {
return UIInterfaceOrientationMask.all
}
// Else only allow the window to support portrait orientation
else {
return UIInterfaceOrientationMask.portrait
}
}
// If the root view controller hasn't been set yet, just
// return anything
return UIInterfaceOrientationMask.portrait
}
Note that if that SpecificViewController is in landscape before going to a portrait screen, the other view will still open in landscape. To circumvent this, I'd recommend disallowing transitions while that view is in landscape.
Swift 3
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> Int {
// Make sure the root controller has been set
// (won't initially be set when the app is launched)
if let navigationController = self.window?.rootViewController as? UINavigationController {
// If the visible view controller is the
// view controller you'd like to rotate, allow
// that window to support all orientations
if navigationController.visibleViewController is SpecificViewController {
return Int(UIInterfaceOrientationMask.All.rawValue)
}
// Else only allow the window to support portrait orientation
else {
return Int(UIInterfaceOrientationMask.Portrait.rawValue)
}
}
// If the root view controller hasn't been set yet, just
// return anything
return Int(UIInterfaceOrientationMask.Portrait.rawValue)
}
You can also do it in a protocol oriented way.
Just create the protocol
protocol CanRotate {
}
Add the the same 2 methods in the AppDelegate in a more "swifty" way
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if topViewController(in: window?.rootViewController) is CanRotate {
return .allButUpsideDown
} else {
return .portrait
}
}
func topViewController(in rootViewController: UIViewController?) -> UIViewController? {
guard let rootViewController = rootViewController else {
return nil
}
if let tabBarController = rootViewController as? UITabBarController {
return topViewController(in: tabBarController.selectedViewController)
} else if let navigationController = rootViewController as? UINavigationController {
return topViewController(in: navigationController.visibleViewController)
} else if let presentedViewController = rootViewController.presentedViewController {
return topViewController(in: presentedViewController)
}
return rootViewController
}
And in every ViewController that you want a different behaviour, just add the protocol name in the definition of the class.
class ViewController: UIViewController, CanRotate {}
If you want any particular combination, they you can add to the protocol a variable to override
protocol CanRotate {
var supportedInterfaceOrientations: UIInterfaceOrientationMask
}
Sometimes when you're using a custom navigation flow (that may get really complex) the above-mentioned solutions may not always work. Besides, if you have several ViewControllers that need support for multiple orientations it may get quite tedious.
Here's a rather quick solution I found. Define a class OrientationManager and use it to update supported orientations in AppDelegate:
class OrientationManager {
static var landscapeSupported: Bool = false
}
Then in AppDelegate put the orientations you want for that specific case:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if OrientationManager.landscapeSupported {
return .allButUpsideDown
}
return .portrait
}
Then in the ViewControllers that you want to have multiple navigations update the OrientationManager:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
OrientationManager.landscapeSupported = true
}
Also, don't forget to update it once again when you'll be exiting this ViewController:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
OrientationManager.landscapeSupported = false
//The code below will automatically rotate your device's orientation when you exit this ViewController
let orientationValue = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(orientationValue, forKey: "orientation")
}
Hope this helps!
Update:
You may just want to add a static func to your Orientation Support Manager class:
static func setOrientation(_ orientation: UIInterfaceOrientation) {
let orientationValue = orientation.rawValue
UIDevice.current.setValue(orientationValue, forKey: "orientation")
landscapeSupported = orientation.isLandscape
}
Then you can call this function whenever you need to set the orientation back to portrait. That will also update the static landscapeSupported value:
OSM.setOrientation(.portrait)
This is for Swift 4 and Swift 5. You can use the follow code in your AppDelegate.swift :
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
guard let rootViewController = self.topViewControllerWithRootViewController(rootViewController: window?.rootViewController),
(rootViewController.responds(to: Selector(("canRotate")))) else {
// Only allow portrait (standard behaviour)
return .portrait;
}
// Unlock landscape view orientations for this view controller
return .allButUpsideDown;
}
private func topViewControllerWithRootViewController(rootViewController: UIViewController!) -> UIViewController? {
guard rootViewController != nil else { return nil }
guard !(rootViewController.isKind(of: (UITabBarController).self)) else{
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UITabBarController).selectedViewController)
}
guard !(rootViewController.isKind(of:(UINavigationController).self)) else{
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UINavigationController).visibleViewController)
}
guard !(rootViewController.presentedViewController != nil) else {
return topViewControllerWithRootViewController(rootViewController: rootViewController.presentedViewController)
}
return rootViewController
}
You can then make a custom UIViewController rotate by overriding shouldAutorotate
With everyone's ideas I wrote the most elegant way to do it I think.
Result:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return (UIApplication.getTopViewController() as? Rotatable == nil) ? .portrait : .allButUpsideDown
}
Add this extension to your project which will always be useful not only for this:
extension UIApplication {
class func getTopViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return getTopViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return getTopViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return getTopViewController(base: presented)
}
return base
}
}
Create the protocol:
protocol Rotatable {}
And implement it:
class ViewController: UIViewController, Rotatable {
}
Use the shouldAutorotate and the supportedInterfaceOrientations method in the ViewController you want to display in landscape and portrait mode:
This method should override the storyboard-settings.
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
return UIInterfaceOrientation.Portrait.rawValue | UIInterfaceOrientation.LandscapeLeft.rawValue | UIInterfaceOrientation.LandscapeRight.rawValue
}
I just faced a very similar problem where I wanted to present a video player in portrait and landscape mode whereas the rest of the app is portrait only. My main problem was that when I dismissed the video vc in landscape mode the presenting vc was only briefly in landscape mode.
As pointed out in the comment to #Lyndsey Scott's answer this can be circumvented by disallowing transitions while in landscape mode, but by combining this and this I've found a better and more generic solution (IMO). This solution allows rotation in all vc where you put canRotate(){} and doesn't rotate the presenting vc.
Swift 3:
In AppDelegate.swift:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let rootViewController = self.topViewControllerWithRootViewController(rootViewController: window?.rootViewController) {
if (rootViewController.responds(to: Selector(("canRotate")))) {
// Unlock landscape view orientations for this view controller if it is not currently being dismissed
if !rootViewController.isBeingDismissed{
return .allButUpsideDown
}
}
}
// Only allow portrait (standard behaviour)
return .portrait
}
private func topViewControllerWithRootViewController(rootViewController: UIViewController!) -> UIViewController? {
if (rootViewController == nil) {
return nil
}
if (rootViewController.isKind(of: UITabBarController.self)) {
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UITabBarController).selectedViewController)
} else if (rootViewController.isKind(of: UINavigationController.self)) {
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UINavigationController).visibleViewController)
} else if (rootViewController.presentedViewController != nil) {
return topViewControllerWithRootViewController(rootViewController: rootViewController.presentedViewController)
}
return rootViewController
}
In each view controller where rotation should be allowed:
func canRotate(){}
Swift 5 using Marker protocol
Combined version of several answers here, done in what I think is a more readable/elegant implementation. (Derived from earlier answers here, not original work by me!)
protocol RotatableViewController {
// No content necessary, marker protocol
}
class MyViewController: UIViewController, RotatableViewController {
// normal content... nothing more required
}
extension AppDelegate {
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
guard
let rootVc = self.topViewControllerWithRootViewController(rootViewController: window?.rootViewController),
rootVc.isBeingDismissed == false,
let _ = rootVc as? RotatableViewController
else {
return .portrait // Some condition not met, so default answer for app
}
// Conditions met, is rotatable:
return .allButUpsideDown
}
private func topViewControllerWithRootViewController(rootViewController: UIViewController!) -> UIViewController? {
if (rootViewController == nil) {
return nil
}
if (rootViewController.isKind(of: UITabBarController.self)) {
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UITabBarController).selectedViewController)
}
else if (rootViewController.isKind(of: UINavigationController.self)) {
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UINavigationController).visibleViewController)
}
else if (rootViewController.presentedViewController != nil) {
return topViewControllerWithRootViewController(rootViewController: rootViewController.presentedViewController)
}
return rootViewController
}
}
Swift 3:
Add code to AppDelegate.swift
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let rootViewController = self.topViewControllerWithRootViewController(rootViewController: window?.rootViewController) {
if (rootViewController.responds(to: Selector(("canRotate")))) {
// Unlock landscape view orientations for this view controller
return .allButUpsideDown;
}
}
// Only allow portrait (standard behaviour)
return .portrait;
}
private func topViewControllerWithRootViewController(rootViewController: UIViewController!) -> UIViewController? {
if (rootViewController == nil) { return nil }
if (rootViewController.isKind(of: (UITabBarController).self)) {
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UITabBarController).selectedViewController)
} else if (rootViewController.isKind(of:(UINavigationController).self)) {
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UINavigationController).visibleViewController)
} else if (rootViewController.presentedViewController != nil) {
return topViewControllerWithRootViewController(rootViewController: rootViewController.presentedViewController)
}
return rootViewController
}
Then :
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillDisappear(_ animated : Bool) {
super.viewWillDisappear(animated)
if (self.isMovingFromParentViewController) {
UIDevice.current.setValue(Int(UIInterfaceOrientation.portrait.rawValue), forKey: "orientation")
}
}
func canRotate() -> Void {}
}
http://www.jairobjunior.com/blog/2016/03/05/how-to-rotate-only-one-view-controller-to-landscape-in-ios-slash-swift/
SWIFT 4
For UITabBarController can we use this line of code in AppDelegate.swift.
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let tabBarController = window?.rootViewController as? UITabBarController {
if let tabBarViewControllers = tabBarController.viewControllers {
if let projectsNavigationController = tabBarViewControllers[1] as? UINavigationController {
if projectsNavigationController.visibleViewController is PickerViewController //use here your own ViewController class name {
return .all
}
}
}
}
return .portrait
}
Solution Swift 5.1
In App delegate implement this method
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let topController = UIApplication.topViewController() {
if topController.isKind(of: YourSpecificViewController.self) {
return .all
}
return .portrait
}
return .portrait
}
Then add this extension to get the top most ViewController
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
} else if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
return topViewController(base: selected)
} else if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
Just wanted to share my solution as someone who has spent too much time rotating one view controller in the app:
var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { get }
overriding this UIViewController method helped me do what I need.
On the view controller that you want to rotate do this for landscape left rotation:
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return UIInterfaceOrientation.landscapeLeft
}
Make sure you enable rotation in the desired directions from the project settings:
And add this to AppDelegate to disable other screens' rotation:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return .portrait
}
Swift 5
Another answer, this one covers the isBeingDismissed case.
In AppDelegate:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if
let vvc = navigationController?.visibleViewController,
vvc is YOURViewControllerClassName &&
!vvc.isBeingDismissed
{
return UIInterfaceOrientationMask.landscape
} else {
return UIInterfaceOrientationMask.portrait
}
}
None of these answers worked for me. Fundamentally, AppDelegate's method does not allow specification on which viewController. So either the topMost ViewController is rotatable, in which case the whole view controller hierarchy gets rotated, or nothing gets rotated.
However, I did find a promising answer in Child View Controller to Rotate While Parent View Controller Does Not
It references https://developer.apple.com/library/archive/qa/qa1890/_index.html