Swift: cómo detectar cambios de orientación

96

Quiero agregar dos imágenes a la vista de una sola imagen (es decir, para una imagen horizontal y otra imagen para el retrato), pero no sé cómo detectar cambios de orientación usando idiomas rápidos.

Probé esta respuesta pero solo toma una imagen

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    if UIDevice.currentDevice().orientation.isLandscape.boolValue {
        print("Landscape")
    } else {
        print("Portrait")
    }
}

Soy nuevo en el desarrollo de iOS, ¡cualquier consejo sería muy apreciado!

Raghuram
fuente
1
¿Podría editar su pregunta y formatear su código? Puede hacerlo con cmd + k cuando haya seleccionado su código.
jbehrens94
2
Mira
¿Imprime correctamente el paisaje / retrato?
Daniel
sí, se imprime correctamente @simpleBob
Raghuram
Debe usar la orientación de la interfaz en lugar de la orientación del dispositivo => stackoverflow.com/a/60577486/8780127
Wilfried Josset

Respuestas:

121
let const = "Background" //image name
let const2 = "GreyBackground" // image name
    @IBOutlet weak var imageView: UIImageView!
    override func viewDidLoad() {
        super.viewDidLoad()

        imageView.image = UIImage(named: const)
        // Do any additional setup after loading the view.
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        if UIDevice.current.orientation.isLandscape {
            print("Landscape")
            imageView.image = UIImage(named: const2)
        } else {
            print("Portrait")
            imageView.image = UIImage(named: const)
        }
    }
Rutvik Kanbargi
fuente
Gracias, funciona para imageView, ¿qué pasa si quiero agregar una imagen de fondo para viewController?
Raghuram
1
Ponga un imageView en segundo plano. Dar restricción a imageView para la parte superior, inferior, inicial, final de la vista principal de ViewController para cubrir todo el fondo.
Rutvik Kanbargi
4
No olvide llamar a super.viewWillTransitionToSize dentro de su método de anulación
Brian Sachetta
¿Sabes por qué no puedo anular viewWillTransitioncuando hago subclases UIView?
Xcoder
2
Hay otro tipo de orientación: .isFlat. Si solo desea capturar, portraitle sugiero que cambie la cláusula else a else if UIDevice.current.orientation.isPortrait. Nota: .isFlatpuede suceder tanto en retrato como en paisaje, y con solo más {} estará predeterminado allí siempre (incluso si es plano).
PMT
78

Usando NotificationCenter y UIDevice 'sbeginGeneratingDeviceOrientationNotifications

Swift 4.2+

override func viewDidLoad() {
    super.viewDidLoad()        

    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
}

deinit {
   NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)         
}

func rotated() {
    if UIDevice.current.orientation.isLandscape {
        print("Landscape")
    } else {
        print("Portrait")
    }
}

Swift 3

override func viewDidLoad() {
    super.viewDidLoad()        

    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}

deinit {
     NotificationCenter.default.removeObserver(self)
}

func rotated() {
    if UIDevice.current.orientation.isLandscape {
        print("Landscape")
    } else {
        print("Portrait")
    }
}
maslovsa
fuente
1
Definitivamente, mi enfoque necesita código adicional. Pero realmente "detecta cambios de orientación".
maslovsa
3
@Michael Porque viewWillTransition:anticipa que el cambio ocurrirá mientras UIDeviceOrientationDidChangese llama al uso una vez que se complete el cambio.
Lyndsey Scott
1
@LyndseyScott Sigo creyendo que el comportamiento descrito se puede lograr sin usar el NSNotificationCenter potencialmente peligroso. Revise la siguiente respuesta y déjeme saber lo que piensa
Michael
4
Lo interesante de esta respuesta es que dará la orientación actual incluso cuando el controlador de vista shouldAutorotateesté configurado en false. Esto es útil para pantallas como la pantalla principal de la aplicación Cámara: la orientación está bloqueada pero los botones pueden girar.
yoninja
1
A partir de iOS 9, ni siquiera es necesario llamar explícitamente a deinit. Puede leer más sobre esto aquí: useyourloaf.com/blog/…
James Jordan Taylor
63

Swift 3 Código anterior actualizado:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    if UIDevice.current.orientation.isLandscape {
        print("Landscape")
    } else {
        print("Portrait")
    }
}
Yaroslav Bai
fuente
1
¿Sabes por qué no puedo anular viewWillTransition al subclasificar UIView?
Xcoder
Esto se bloqueará si intenta hacer referencia a cualquiera de las vistas en el controlador.
James Jordan Taylor
@JamesJordanTaylor, ¿podría explicar por qué se bloqueará?
Orium
Fue hace 6 meses cuando comenté, pero creo que la razón que dio Xcode cuando lo intenté fue una excepción de puntero nulo porque la vista en sí aún no se había instanciado, solo el viewController. Aunque podría estar recordando mal.
James Jordan Taylor
@Xcoder, esta es una función en UIViewController, no en UIView, por eso no puede anularla.
LightningStryk
25

⚠️ Orientación del dispositivo! = Orientación de la interfaz⚠️

Swift 5. * iOS14 y versiones anteriores

Realmente deberías marcar la diferencia entre:

  • Orientación del dispositivo => Indica la orientación del dispositivo físico
  • Orientación de la interfaz => Indica la orientación de la interfaz que se muestra en la pantalla

Hay muchos escenarios en los que esos 2 valores no coinciden, como:

  • Cuando bloquea la orientación de la pantalla
  • Cuando tienes tu dispositivo plano

En la mayoría de los casos, querrá usar la orientación de la interfaz y puede obtenerla a través de la ventana:

private var windowInterfaceOrientation: UIInterfaceOrientation? {
    return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
}

En caso de que también desee admitir <iOS 13 (como iOS 12), debe hacer lo siguiente:

private var windowInterfaceOrientation: UIInterfaceOrientation? {
    if #available(iOS 13.0, *) {
        return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
    } else {
        return UIApplication.shared.statusBarOrientation
    }
}

Ahora necesita definir dónde reaccionar al cambio de orientación de la interfaz de la ventana. Hay varias formas de hacerlo, pero la solución óptima es hacerlo dentro willTransition(to newCollection: UITraitCollection.

Este método UIViewController heredado, que se puede anular, se activará cada vez que cambie la orientación de la interfaz. En consecuencia, puede hacer todas sus modificaciones en este último.

Aquí hay un ejemplo de solución :

class ViewController: UIViewController {
    override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
        super.willTransition(to: newCollection, with: coordinator)
        
        coordinator.animate(alongsideTransition: { (context) in
            guard let windowInterfaceOrientation = self.windowInterfaceOrientation else { return }
            
            if windowInterfaceOrientation.isLandscape {
                // activate landscape changes
            } else {
                // activate portrait changes
            }
        })
    }
    
    private var windowInterfaceOrientation: UIInterfaceOrientation? {
        return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
    }
}

Al implementar este método, podrá reaccionar ante cualquier cambio de orientación de su interfaz. Pero tenga en cuenta que no se activará al abrir la aplicación, por lo que también tendrá que actualizar manualmente su interfaz en formato viewWillAppear().

Creé un proyecto de muestra que subraya la diferencia entre la orientación del dispositivo y la orientación de la interfaz. Además, le ayudará a comprender los diferentes comportamientos según el paso del ciclo de vida que decida actualizar su interfaz de usuario.

Siéntase libre de clonar y ejecutar el siguiente repositorio: https://github.com/wjosset/ReactToOrientation

Wilfried Josset
fuente
¿Cómo hacer esto antes de iOS 13?
Daniel Springer
1
@DanielSpringer gracias por su comentario, he editado la publicación para que sea compatible con iOS 12 y
versiones anteriores
1
Encuentro esta respuesta la mejor y más informativa. Pero probar en iPadOS13.5 el uso de sugerido func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator)no funcionó. Lo hice funcionar usando func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator).
Andrej
12

Swift 4+: estaba usando esto para el diseño de teclado virtual y, por alguna razón, el UIDevice.current.orientation.isLandscapemétodo me decía que lo era Portrait, así que esto es lo que usé en su lugar:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    if(size.width > self.view.frame.size.width){
        //Landscape
    }
    else{
        //Portrait
    }
}
asetniop
fuente
bueno, tengo un problema similar en mi aplicación iMessage. ¿No es realmente extraño? ¡E incluso tu solución funciona al revés! ¯_ (ツ) _ / ¯
Shyam
No use UIScreen.main.bounds, porque podría darle los límites de la pantalla antes de la transición (tenga en cuenta el 'Will' en el método), especialmente en múltiples rotaciones rápidas. Deberías usar el parámetro 'tamaño' ...
Dan Bodnar
@ dan-bodnar ¿Te refieres al parámetro nativeBounds?
asetniop
3
@asetniop, no. El sizeparámetro que se inyecta en viewWillTransitionToSize: withCoordinator: método que escribió anteriormente. Esto reflejará el tamaño exacto que tendrá su vista después de la transición.
Dan Bodnar
Esta no es una buena solución si está utilizando controladores de vista secundarios, el tamaño del contenedor puede ser siempre cuadrado, independientemente de la orientación.
bojan
8

Swift 4.2, RxSwift

Si necesitamos volver a cargar collectionView.

NotificationCenter.default.rx.notification(UIDevice.orientationDidChangeNotification)
    .observeOn(MainScheduler.instance)
    .map { _ in }            
    .bind(to: collectionView.rx.reloadData)
    .disposed(by: bag)

Rápido 4, RxSwift

Si necesitamos volver a cargar collectionView.

NotificationCenter.default.rx.notification(NSNotification.Name.UIDeviceOrientationDidChange)
    .observeOn(MainScheduler.instance)
    .map { _ in }            
    .bind(to: collectionView.rx.reloadData)
    .disposed(by: bag)
maslovsa
fuente
5

Creo que la respuesta correcta es en realidad una combinación de ambos enfoques: viewWIllTransition(toSize:)y NotificationCenter's UIDeviceOrientationDidChange.

viewWillTransition(toSize:)le notifica antes de la transición.

NotificationCenter UIDeviceOrientationDidChangele notifica después .

Tienes que ser muy cuidadoso. Por ejemplo, UISplitViewControllercuando el dispositivo gira en ciertas orientaciones, DetailViewControllerse saca de la matriz del UISplitViewController' viewcontrollersy se empuja al maestro UINavigationController. Si busca el controlador de vista de detalles antes de que finalice la rotación, es posible que no exista y se bloquee.

MH175
fuente
5

Si está utilizando la versión Swift> = 3.0, hay algunas actualizaciones de código que debe aplicar, como ya han dicho otros. No olvides llamar a super:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

   super.viewWillTransition(to: size, with: coordinator)

   // YOUR CODE OR FUNCTIONS CALL HERE

}

Si está pensando en utilizar un StackView para sus imágenes, tenga en cuenta que puede hacer algo como lo siguiente:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

   super.viewWillTransition(to: size, with: coordinator)

   if UIDevice.current.orientation.isLandscape {

      stackView.axis = .horizontal

   } else {

      stackView.axis = .vertical

   } // else

}

Si está utilizando Interface Builder, no olvide seleccionar la clase personalizada para este objeto UIStackView, en la sección Identity Inspector en el panel derecho. Luego simplemente cree (también a través de Interface Builder) la referencia de IBOutlet a la instancia personalizada de UIStackView:

@IBOutlet weak var stackView: MyStackView!

Toma la idea y adáptala a tus necesidades. ¡Espero que esto le pueda ayudar!

WeiseRatel
fuente
Buen punto sobre llamar super. Es realmente una codificación descuidada no llamar al súper método en una función anulada y puede producir un comportamiento impredecible en las aplicaciones. ¡Y potencialmente errores que son realmente difíciles de rastrear!
Adam Freeman
¿Sabes por qué no puedo anular viewWillTransition al subclasificar UIView?
Xcoder
@Xcoder La razón (generalmente) de un objeto que no aplica una anulación radica en que la instancia en tiempo de ejecución no se creó con la clase personalizada sino con la predeterminada. Si está utilizando Interface Builder, asegúrese de seleccionar la clase personalizada para este objeto UIStackView, en la sección Inspector de identidad en el panel derecho.
WeiseRatel
5

Rápido 4

Tuve algunos problemas menores al actualizar la vista ViewControllers usando UIDevice.current.orientation, como actualizar las restricciones de las celdas de la vista de tabla durante la rotación o animación de las subvistas.

En lugar de los métodos anteriores, actualmente estoy comparando el tamaño de transición con el tamaño de la vista de los controladores de vista. Este parece ser el camino correcto a seguir, ya que uno tiene acceso a ambos en este punto del código:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    print("Will Transition to size \(size) from super view size \(self.view.frame.size)")

    if (size.width > self.view.frame.size.width) {
        print("Landscape")
    } else {
        print("Portrait")
    }

    if (size.width != self.view.frame.size.width) {
        // Reload TableView to update cell's constraints.
    // Ensuring no dequeued cells have old constraints.
        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }


}

Salida en un iPhone 6:

Will Transition to size (667.0, 375.0) from super view size (375.0, 667.0) 
Will Transition to size (375.0, 667.0) from super view size (667.0, 375.0)
RLoniello
fuente
¿Debería activar el soporte de orientación desde el nivel del proyecto?
Ericpoon
3

Todas las contribuciones anteriores están bien, pero una pequeña nota:

a) si la orientación se establece en plist, solo retrato o ejemplo, no se le notificará a través de viewWillTransition

b) si de todos modos necesitamos saber si el usuario ha girado el dispositivo (por ejemplo, un juego o similar ..) solo podemos usar:

NotificationCenter.default.addObserver(self, selector: #selector(ViewController.rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)

probado en Xcode8, iOS11

ingconti
fuente
2

Puede usar viewWillTransition(to:with:)y tocar animate(alongsideTransition:completion:)para obtener la orientación de la interfaz DESPUÉS de que se complete la transición. Solo tiene que definir e implementar un protocolo similar a este para aprovechar el evento. Tenga en cuenta que este código se usó para un juego SpriteKit y su implementación específica puede diferir.

protocol CanReceiveTransitionEvents {
    func viewWillTransition(to size: CGSize)
    func interfaceOrientationChanged(to orientation: UIInterfaceOrientation)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)

        guard
            let skView = self.view as? SKView,
            let canReceiveRotationEvents = skView.scene as? CanReceiveTransitionEvents else { return }

        coordinator.animate(alongsideTransition: nil) { _ in
            if let interfaceOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation {
                canReceiveRotationEvents.interfaceOrientationChanged(to: interfaceOrientation)
            }
        }

        canReceiveRotationEvents.viewWillTransition(to: size)
    }

Puede establecer puntos de interrupción en estas funciones y observar que interfaceOrientationChanged(to orientation: UIInterfaceOrientation)siempre se llama después viewWillTransition(to size: CGSize)con la orientación actualizada.

El Rossinator
fuente
1

Para obtener la orientación correcta al iniciar la aplicación , debe verificarla viewDidLayoutSubviews(). Otros métodos descritos aquí no funcionarán.

Aquí tienes un ejemplo de cómo hacerlo:

var mFirstStart = true

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    if (mFirstStart) {
        mFirstStart = false
        detectOrientation()
    }
}

func detectOrientation() {
    if UIDevice.current.orientation.isLandscape {
        print("Landscape")
        // do your stuff here for landscape
    } else {
        print("Portrait")
        // do your stuff here for portrait
    }
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    detectOrientation()
}

Esto funcionará siempre, en el primer inicio de la aplicación y si gira mientras la aplicación se está ejecutando .

lenooh
fuente
0

Otra forma de detectar las orientaciones de los dispositivos es con la función traitCollectionDidChange (_ :). El sistema llama a este método cuando cambia el entorno de la interfaz de iOS.

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
{
    super.traitCollectionDidChange(previousTraitCollection)
    //...
}

Además, puede usar la función willTransition (to: with :) (que se llama antes de traitCollectionDidChange (_ :)), para obtener información justo antes de que se aplique la orientación.

 override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator)
{
    super.willTransition(to: newCollection, with: coordinator)
    //...
}
ckc
fuente