Disable autorotate on a single subview in iOS8 - ios

I'm writing a drawing app and I don't want the drawing view to rotate. At the same time, I want other controls to rotate nicely depending on the orientation of the device. In iOS 7 I've solved this via:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
float rotation;
if (toInterfaceOrientation==UIInterfaceOrientationPortrait) {
rotation = 0;
}
else if (toInterfaceOrientation==UIInterfaceOrientationLandscapeLeft) {
rotation = M_PI/2;
} else if (toInterfaceOrientation==UIInterfaceOrientationLandscapeRight) {
rotation = -M_PI/2;
}
self.drawingView.transform = CGAffineTransformMakeRotation(rotation);
self.drawingView.frame = self.view.frame;
}
But on iOS 8 even though the function is called and the transform is set correctly, it does not prevent the view from rotating.
I've tried creating a view controller which simply prevents the rotation of it's view and add it's view to the view hierarchy, but then it doesn't respond to user input.
Any ideas?
Thanks!

Okay after some fighting with subviews and transitionCoordinators I've finally figured it out:
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
CGAffineTransform targetRotation = [coordinator targetTransform];
CGAffineTransform inverseRotation = CGAffineTransformInvert(targetRotation);
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
self.drawingView.transform = CGAffineTransformConcat(self.drawingView.transform, inverseRotation);
self.drawingView.frame = self.view.bounds;
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
}];
}
What I do is, I calculate the inverse of the transform applied to the view and then use it to change the transform. furthermore I change the frame with the view bounds. This is due to it being full screen.

Dimitri's answer worked perfectly for me. This is the swift version of the code in case someone needs it...
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
let targetRotation = coordinator.targetTransform()
let inverseRotation = CGAffineTransformInvert(targetRotation)
coordinator.animateAlongsideTransition({ context in
self.drawingView.transform = CGAffineTransformConcat(self.drawingView.transform, inverseRotation)
self.drawingView.frame = self.view.bounds
context.viewControllerForKey(UITransitionContextFromViewControllerKey)
}, completion: nil)
}

Swift 4 has a lot of updates, including the viewWillTransition function.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let targetRotation = coordinator.targetTransform
let inverseRotation = targetRotation.inverted()
coordinator.animate(alongsideTransition: { context in
self.drawingView.transform = self.drawingView.transform.concatenating(inverseRotation)
self.drawingView.frame = self.view.bounds
context.viewController(forKey: UITransitionContextViewControllerKey.from)
}, completion: nil)
}

I managed to obtain this, check:
rotation working as desired
Note: The green and the red views are subviews of the controller's view. The blue view is subview of the red view.
Idea
According to https://developer.apple.com/library/archive/qa/qa1890/_index.html, we need to apply an inverse rotation to the view when it is transitioning to a new size.
In addition to that, we need to adjust the constraints after rotation (landscape/portrait).
Implementation
class MyViewController: UIViewController {
var viewThatShouldNotRotate = UIView()
var view2 = UIView()
var insiderView = UIView()
var portraitConstraints: [NSLayoutConstraint]!
var landscapeConstraints: [NSLayoutConstraint]!
override func viewDidLoad() {
super.viewDidLoad()
viewThatShouldNotRotate.backgroundColor = .red
view2.backgroundColor = .green
insiderView.backgroundColor = .blue
view.addSubview(viewThatShouldNotRotate)
view.addSubview(view2)
viewThatShouldNotRotate.addSubview(insiderView)
portraitConstraints = createConstraintsForPortrait()
landscapeConstraints = createConstraintsForLandscape()
}
func createConstraintsForLandscape() -> [NSLayoutConstraint] {
return NSLayoutConstraint.autoCreateConstraintsWithoutInstalling {
viewThatShouldNotRotate.autoMatch(.height, to: .width, of: view)
viewThatShouldNotRotate.autoMatch(.width, to: .height, of: view)
viewThatShouldNotRotate.autoCenterInSuperview()
view2.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(), excludingEdge: .top)
view2.autoSetDimension(.height, toSize: 100)
insiderView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom)
insiderView.autoSetDimension(.height, toSize: 100)
}
}
func createConstraintsForPortrait() -> [NSLayoutConstraint] {
return NSLayoutConstraint.autoCreateConstraintsWithoutInstalling {
viewThatShouldNotRotate.autoMatch(.height, to: .height, of: view)
viewThatShouldNotRotate.autoMatch(.width, to: .width, of: view)
viewThatShouldNotRotate.autoCenterInSuperview()
view2.autoPinEdges(toSuperviewMarginsExcludingEdge: .top)
view2.autoSetDimension(.height, toSize: 100)
insiderView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom)
insiderView.autoSetDimension(.height, toSize: 100)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if view.bounds.size.width > view.bounds.size.height {
// landscape
portraitConstraints.forEach {$0.autoRemove()}
landscapeConstraints.forEach { $0.autoInstall() }
} else {
landscapeConstraints.forEach {$0.autoRemove()}
portraitConstraints.forEach { $0.autoInstall() }
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let viewToRotate: UIView = viewThatShouldNotRotate
coordinator.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) -> Void in
let deltaTransform = coordinator.targetTransform
let deltaAngle = atan2f(Float(deltaTransform.b), Float(deltaTransform.a))
var currentRotation = (viewToRotate.layer.value(forKeyPath: "transform.rotation.z") as! NSNumber).floatValue
// Adding a small value to the rotation angle forces the animation to occur in a the desired direction, preventing an issue where the view would appear to rotate 2PI radians during a rotation from LandscapeRight -> LandscapeLeft.
currentRotation = currentRotation + (-1 * Float(deltaAngle)) + 0.0001
viewToRotate.layer.setValue(currentRotation, forKeyPath:"transform.rotation.z")
}) { (UIViewControllerTransitionCoordinatorContext) -> Void in
var currentTransform = viewToRotate.transform;
currentTransform.a = round(currentTransform.a);
currentTransform.b = round(currentTransform.b);
currentTransform.c = round(currentTransform.c);
currentTransform.d = round(currentTransform.d);
viewToRotate.transform = currentTransform;
}
}
}

In iOS 8 transforms aren't applied to individual views owned by view controllers. Instead the rotation transforms are applied to the UIWindow. The result is that developers never see a rotation being applied, but rather a resize.
In iOS 8 you can either override the callbacks for size class changes and perform your own transform there, or you can get the orientation events as described here: https://developer.apple.com/library/prerelease/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/motion_event_basics/motion_event_basics.html.

Related

Prevent MTKView camera feed rotation, but allow other on-screen VCs to rotate

Goal
With an MTKView, replicate the gravity of the AVCaptureVideoPreviewLayer or Apple's Camera app. Video device orientation does not change. The camera feed's edges do not budge a pixel, never revealing the screen background. Other on-screen VCs rotate normally.
Observed
Applying Tech QA 1890's transform during viewWillTransition, the MTKView does counter-rotate... BUT that rotation is still uncomfortably visible. The edges of the view come unpinned during the animation, masking some camera pixels and showing a white background set for the VC holding the MTKView.
Question
How can I make those edges stick to screen bounds like a scared clam?
I assume my error is in constraints, but I'm open to being wrong in other ways. :)
View Hierarchy
A tiny camera filter app has an overlay of camera controls (VC #1) atop an MTKView (in VC #2) pinned to the screen's edges.
UINavigationController
│
└─ CameraScreenVC
│
├── CameraControlsVC <- Please rotate subviews
│
└── MetalCameraFeedVC
└── MTKView <- Please no rotation edges
Code
Buildable demo repo
Relevant snippets below.
MetalCameraVC.swift
final class MetalCameraVC: UIViewController {
let mtkView = MTKView() // This VC's only view
/// Called in viewDidAppear
func setupMetal(){
metalDevice = MTLCreateSystemDefaultDevice()
mtkView.device = metalDevice
mtkView.isPaused = true
mtkView.enableSetNeedsDisplay = false
metalCommandQueue = metalDevice.makeCommandQueue()
mtkView.delegate = self
mtkView.framebufferOnly = false
ciContext = CIContext(
mtlDevice: metalDevice,
options: [.workingColorSpace: CGColorSpace(name: CGColorSpace.sRGB)!])
}
...
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
// blank
}
func draw(in mtkview: MTKView) {
image = image.transformed(by: scaleToScreenBounds)
image = image.cropped(to: mtkview.drawableSize.zeroOriginRect())
guard let buffer = metalCommandQueue.makeCommandBuffer(),
let currentDrawable = mtkview.currentDrawable
else { return }
ciContext.render(image,
to: currentDrawable.texture,
commandBuffer: buffer,
bounds: mtkview.drawableSize.zeroOriginRect(),
colorSpace: CGColorSpaceCreateDeviceRGB())
buffer.present(currentDrawable)
buffer.commit()
}
}
extension MetalCameraVC {
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(mtkView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
mtkView.frame = view.frame
if let orientation = AVCaptureVideoOrientation.fromCurrentDeviceOrientation() {
lastOrientation = orientation
}
}
}
+Rotation
/// Apple Technical QA 1890 Prevent View From Rotating
extension MetalCameraVC {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
mtkView.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { [self] context in
let delta = coordinator.targetTransform
let deltaAngle = atan2(delta.b, delta.a)
var currentAngle = mtkView.layer.value(forKeyPath: "transform.rotation.z") as? CGFloat ?? 0
currentAngle += -1 * deltaAngle + 0.1
mtkView.layer.setValue(currentAngle, forKeyPath: "transform.rotation.z")
} completion: { [self] context in
var rounded = mtkView.transform
rounded.a = round(rounded.a)
rounded.b = round(rounded.b)
rounded.c = round(rounded.c)
rounded.d = round(rounded.d)
mtkView.transform = rounded
}
}
}
I think the "easiest" solution is to actually prevent your UI from rotating and observing device orientation changes to manually rotate only the interface elements you want to rotate.
In your view controller, you can override shouldAutorotate to prevent auto-rotation and use supportedInterfaceOrientations to only return the one allowed orientation.
Rotating UI elements manually can be done using their transform property.

How to exactly replicate native Camera.app UI?

I'm currently implementing a custom camera app, and it turns out that replicating Camera.app's UI is quite tricky!
The question that bothers me the most is of course autorotation. Camera.app creates an illusion that UI doesn't rotate, except for some buttons, but it does rotate! This is clear, because:
Home indicator also rotates with the device
Control Center and Notification Center can be pulled down from the top
According to this Q&A, looks like Camera.app uses viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator), and rotates the elements in opposite direction to create the illusion that nothing actually rotates. But there's a catch: to achieve this effect, views should not use Auto Layout, which makes lots of things more complicated than needed.
Using sample code from the aforementioned Q&A I created a simple app that uses a UIImageView with a screenshot of the Camera.app.
class ViewController: UIViewController {
let imageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(imageView)
imageView.image = UIImage(named: "camera_screenshot.PNG")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
imageView.frame = view.bounds
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.imageView.center = CGPoint(x: self.view.bounds.midX, y: self.view.bounds.midY)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { context in
let deltaTransform = coordinator.targetTransform
let deltaAngle = atan2(deltaTransform.b, deltaTransform.a)
if let currentRotation = self.imageView.layer.value(forKeyPath: "transform.rotation.z") as? CGFloat {
let newRotation = currentRotation + (-1 * deltaAngle + 0.0001)
self.imageView.layer.setValue(newRotation, forKeyPath: "transform.rotation.z")
}
} completion: { context in
// Integralize the transform to undo the extra 0.0001 added to the rotation angle.
var currentTransform = self.imageView.transform
currentTransform.a = round(currentTransform.a)
currentTransform.b = round(currentTransform.b)
currentTransform.c = round(currentTransform.c)
currentTransform.d = round(currentTransform.d)
self.imageView.transform = currentTransform
}
}
override var prefersStatusBarHidden: Bool {
return true
}
}
Good! But not good enough! As you can see in this video the image view doesn't rotate but there's still system animation of rotating the UIWindow (I guess?). Interestingly enough, this system animation appears on iPhone 7 (iOS 13.4.1), but doesn't appear on iPhone 11 Pro (iOS 14.3).
So my questions are:
How to get rid of that "system animation"?
Is it possible to replicate Camera.app UI WITH Auto Layout?
Note: please, do not suggest the approach with multiple UIWindow's because it feels quite hacky to me.

iOS 12 (and under) orientation rotation

let's say we have 2 vc, vc1 has the top half of the screen, and vc2 has bottom half in portrait mode, when the device is rotated to the landscape mode, vc1 has to occupy the left half of the screen and vc2 the right one.
all good for ios 13, the transition goes smoothly, but for ios12 and under, vc1 and vc2 overlap at transition...
here is what i've did:
override func viewDidLoad() {
super.viewDidLoad()
setOrietation()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animateAlongsideTransition(in: nil, animation:
{
[weak self] _ in
self?.setOrietation()
}, completion: nil)
}
private func setOrietation() {
if UIScreen.main.bounds.width > UIScreen.main.bounds.height { // landscape
vc1BottomConstraint.constant = 0
vc1TrailingConstraint.constant = 450
vc2TopConstraint.isActive = false
vc2TopConstraint = vc2.topAnchor.constraint(equalTo: view.topAnchor)
vc2TopConstraint.isActive = true
vc2LeftConstraint.isActive = false
vc2LeftConstraint = vc2.leftAnchor.constraint(equalTo: vc1.rightAnchor)
vc2LeftConstraint.isActive = true
} else {
vc1BottomConstraint.constant = 512
vc1TrailingConstraint.constant = 0
vc2TopConstraint.isActive = false
vc2TopConstraint = vc2.topAnchor.constraint(equalTo: vc1.bottomAnchor)
vc2TopConstraint.isActive = true
vc2LeftConstraint.isActive = false
vc2LeftConstraint = vc2.leftAnchor.constraint(equalTo: view.leftAnchor)
vc2LeftConstraint.isActive = true
}
}
i also tried the same without using constraints and focusing on frame... still the same result
any ideas how i can achieve this ?

iOS 13 style UIPresentationController without relying on snapshots?

iOS 13 seems to use a new UIPresentationController for presenting modal view controllers, but one that does not rely on taking snapshots of the presenting view controller (as most / all libraries out there do). The presenting view controller is 'live' and continues to display animations / changes while the modal view controller is showing above a transparent / tinted background.
I'm able to replicate this easily (as the aim is to make a backward compatible version for iOS 10 / 11 / 12 etc) by using a CGAffineTransform on the presenting view controller's view, however frequently while rotating the device, the presenting view begins to de-shape and grow out of bounds seemingly because the system updates its frame while there's an active transform applied to it.
According to the documentation, frame is undefined when there's a transform applied to the view. Given the system seems to be modifying the frame and not me, how do I solve this without ending up with hacky solutions where I'm updating the presenting view's bounds? I need this presentation controller to remain generic since the presenting controller could be any shape or form, and won't necessarily be a full-screen view.
Here's what I have so far - it's a simple UIPresentationController subclass, which seems to work fine, however rotating the device and then dismissing the presented view controller seems to de-shape the presenting view controller's bounds (becomes too wide or shrinks, depending on whether you presented the modal controller while in landscape / portrait)
class SheetPresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
return CGRect(x: 40, y: containerView!.bounds.height / 2, width: containerView!.bounds.width-80, height: containerView!.bounds.height / 2)
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
if let _ = presentingViewController.transitionCoordinator {
// We're transitioning - don't touch the frame yet as it'll
// clash with our transform
} else {
self.presentedView?.frame = self.frameOfPresentedViewInContainerView
}
}
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
containerView?.backgroundColor = .clear
if let coordinator = presentingViewController.transitionCoordinator {
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.containerView?.backgroundColor = UIColor.black.withAlphaComponent(0.3)
// Scale the presenting view
self?.presentingViewController.view.layer.cornerRadius = 16
self?.presentingViewController.view.transform = CGAffineTransform.init(scaleX: 0.9, y: 0.9)
}, completion: nil)
}
}
override func dismissalTransitionWillBegin() {
if let coordinator = presentingViewController.transitionCoordinator {
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.containerView?.backgroundColor = .clear
self?.presentingViewController.view.layer.cornerRadius = 0
self?.presentingViewController.view.transform = .identity
}, completion: nil)
}
}
}
And the Presenting Animation controller:
import UIKit
final class PresentingAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let presentedViewController = transitionContext.viewController(forKey: .to) else {
return
}
let springTiming = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: CGVector(dx:1.0, dy: 1.0))
let animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: springTiming)
let containerView = transitionContext.containerView
containerView.addSubview(presentedViewController.view)
let finalFrameForPresentedView = transitionContext.finalFrame(for: presentedViewController)
presentedViewController.view.frame = finalFrameForPresentedView
// Move it below the screen so it slides up
presentedViewController.view.frame.origin.y = containerView.bounds.height
animator.addAnimations {
presentedViewController.view.frame = finalFrameForPresentedView
}
animator.addCompletion { (animationPosition) in
if animationPosition == .end {
transitionContext.completeTransition(true)
}
}
animator.startAnimation()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
}
As well as the dismissing animation controller:
import UIKit
final class DismissingAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let presentedViewController = transitionContext.viewController(forKey: .from) else {
return
}
guard let presentingViewController = transitionContext.viewController(forKey: .to) else {
return
}
let finalFrameForPresentedView = transitionContext.finalFrame(for: presentedViewController)
let containerView = transitionContext.containerView
let offscreenFrame = CGRect(x: finalFrameForPresentedView.minX, y: containerView.bounds.height, width: finalFrameForPresentedView.width, height: finalFrameForPresentedView.height)
let springTiming = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: CGVector(dx:1.0, dy: 1.0))
let animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: springTiming)
animator.addAnimations {
presentedViewController.view.frame = offscreenFrame
}
animator.addCompletion { (position) in
if position == .end {
// Complete transition
transitionContext.completeTransition(true)
}
}
animator.startAnimation()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
}
Okay I figured it out. It seems iOS 13 does NOT use a scale transform. The moment you do that, as explained, rotating the device will modify the frame of the presenting view and since you've got a transform applied to the view already, the view will resize in unexpected ways and the transform will no longer be valid.
The solution is to instead use a z-axis perspective, which will give you the exact same result, but doing so will survive rotations etc since all you're doing is moving the view back into 3D space (Z-axis), thus effectively zooming it out. Here's the transform that did this for me (Swift):
func calculatePerspectiveTransform() -> CATransform3D {
let eyePosition:Float = 10.0;
var contentTransform:CATransform3D = CATransform3DIdentity
contentTransform.m34 = CGFloat(-1/eyePosition)
contentTransform = CATransform3DTranslate(contentTransform, 0, 0, -2)
return contentTransform
}
Here's an article explaining how this works: https://whackylabs.com/uikit/2014/10/29/add-some-perspective-to-your-uiviews/
In your UIPresenterController, you would need to do the following too in order to handle this transform across rotations properly:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
// Reset transform before we rotate and then apply it again during rotation
if let presentingView = presentingViewController.view {
presentingView.layer.transform = CATransform3DIdentity
}
coordinator.animate(alongsideTransition: { [weak self] (context) in
if let presentingView = self?.presentingViewController.view {
presentingView.layer.transform = self?.calculatePerspectiveTransform() ?? CATransform3DIdentity
}
})
}
Custom presentations are a tricky part of UIKit. Here's what comes to mind, no guarantees ;-)
I would suggest you either try to "commit" the animation on the presenting view - so in the presentationTransitionDidEnd(Bool) callback remove the transform and set appropriate constraints on the presenting view that match what the transform did. Or you could also just animate the constraint changes to mimic a transform.
Presumably you will get a viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) call back to manage the ongoing presentation if a rotation occurs.

Cannot catch orientation changes from "upsidedown" to landscapes on iOS

I need to invalidate my collection view layout when screen orientation changes. Since iPhone X and others have safe area insets; I need my cells' subviews positioned according to screen safe area insets.
If you switch between left-right landscapes from portrait there is no problem. But if you switch from upsidedown I cannot catch the orientation change and automatically cannot
I tried two existing approaches.
First is using NotificationCenter. Second is overriding viewWillTransitionToSize:withTransitionCoordinator: function. It's all the same.
in CollectionViewController:
NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { (notif) in
self.collectionViewLayout.invalidateLayout()
}
in Cell:
override func layoutSubviews() {
super.layoutSubviews()
let orientation = UIDevice.current.orientation
if let insets = UIApplication.shared.keyWindow?.safeAreaInsets {
if orientation == .landscapeLeft {
titleLabelLeadingConstraint.constant = insets.left + 20
titleLabelTrailingConstraint.constant = 20
} else if orientation == .landscapeRight {
titleLabelLeadingConstraint.constant = 20
titleLabelTrailingConstraint.constant = insets.right + 20
} else {
titleLabelLeadingConstraint.constant = 20
titleLabelTrailingConstraint.constant = 20
}
}
print("orin: \(orientation.rawValue) | left: \(titleLabelLeadingConstraint.constant) | right: \(titleLabelTrailingConstraint.constant)")
}
The print() function not being called when you switch from/to upsidedown. But collectionviewlayouts invalidateLayout() function is being called.
As you've already tried, you can detect orientation change using viewWillTransitionToSize:withTransitionCoordinator:.
I don't know your first attempt, however, try this:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
//If you need to invalidate your collectionView layout, do it in this function
super.viewWillTransition(to: size, with: coordinator)
if UIDevice.current.orientation.isLandscape {
//
} else {
//
}
}
If you need to know the difference between landscapeLeft or Right, try your code in this way:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
//If you need to invalidate your collectionView layout, do it in this function
super.viewWillTransition(to: size, with: coordinator)
if let insets = UIApplication.shared.keyWindow?.safeAreaInsets {
if UIDevice.current.orientation == .landscapeLeft {
titleLabelLeadingConstraint.constant = insets.left + 20
titleLabelTrailingConstraint.constant = 20
} else if UIDevice.current.orientation == .landscapeRight {
titleLabelLeadingConstraint.constant = 20
titleLabelTrailingConstraint.constant = insets.right + 20
} else {
titleLabelLeadingConstraint.constant = 20
titleLabelTrailingConstraint.constant = 20
}
}
}
NOTE: You don't need to register an observer
OK. At last, I came up with the thing I did not want to do. Observed orientation changes from the cells' themselves.
override func awakeFromNib() {
super.awakeFromNib()
NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { (notif ) in
// doing my things already stated in the question's itself.
}
}
Now they can observe all the changes. But I do not know if this much registration is a wise thing; even though collection view reuses cells and -in my case- 7 to 8 cells are on screen at most.

Resources