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 ?
Related
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.
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.
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.
I have a tabbar app, and I put the following code in my view controllers:
let orientation = UIDevice.current.orientation
if orientation == .portrait {
self.tabBarItem.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -17.0)
} else if orientation == .landscapeLeft || orientation == .landscapeRight {
self.tabBarItem.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: 0.0) // updating the title positon
}
This changes the offsets for my tab bar items' titles. In landscape, the offsets get reversed automatically, so I have to do it manually. This works, but only when you actually click on a tab bar item and it loads a view controller.
I want to load this code at the start, so it changes the offset when you launch the app (before the view controllers load). Here are some pictures to describe what I said:
When you first launch the app, this is the first view controller, so it loads that code and it changes position:
If you tap on the second item, the second view controller loads and it changes position as well.
I need to load this code for each tab bar item, so it looks like this from the start:
You should subclass UITabBarController and execute the code that changes the title position adjustment after the controller loads (viewDidLoad) and every time the view size/orientation changes (viewWillTransition).
class CustomTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
tabBarItemsUI()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { [weak self] context in
self?.tabBarItemsUI()
})
}
fileprivate func tabBarItemsUI() {
let isLandscape = [.landscapeLeft, .landscapeRight].contains(UIDevice.current.orientation)
tabBar.items?.forEach { $0.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: isLandscape ? 0 : -17) }
}
}
Try running this code in viewWillAppear() function.
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.