How to exactly replicate native Camera.app UI? - ios

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.

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.

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.

Prevent UIView with AVCaptureVideoPreviewLayer from rotating with rest of ViewController

I have a view controller, embedded in a tab bar controller, that, among other things, presents a AVCaptureVideoPreviewLayer in a UIView.
When the device is rotated, I want the view controller to rotate with it- except for the aforementioned UIView.
Unlike this related question, however, I am not just rotating/transforming my other views in the view controller. The other views need to use their configured autolayout rotation behavior.
I've tried several things, including simply setting the video orientation to portrait:
previewLayer.connection.videoOrientation = .portrait
to extracting the UIView to a separate view controller, embedding that view controller into the original view controller, and setting its autoRotation properties
override var shouldAutorotate: Bool {
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
but then I learned here that iOS only looks at the top-level view controller for those properties.
With everything I have tried, the video preview is rotating with the rest of the view controller- ending up sideways.
The only thing that works, but is hacky and sometimes causes the video preview to become misaligned, is this
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if let videoPreviewLayerConnection = previewLayer.connection {
if let newVideoOrientation = AVCaptureVideoOrientation(rawValue: UIApplication.shared.statusBarOrientation.rawValue) {
videoPreviewLayerConnection.videoOrientation = newVideoOrientation
}
}
}
}
I basically need the opposite of this question.
How can I force the video preview to not rotate but also allow the rest of the view controller to rotate normally? (Same behavior as iOS Camera app except that the other UI elements rotate normally instead of the 90° rotation transform)
The following is possibly as hacky as your solution but it looks cleaner visually.
In viewWillTransition I set the affine transform of the previewView to counteract the orientation set by rotating the phone. It looks cleaner than just setting the videoOrientation as the affine transform animates at the same speed as the orientation change. It is done as follows.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let orientation = UIDevice.current.orientation
var rotation : CGFloat = self.previewView.transform.rotation
switch(orientation) {
case .portrait:
rotation = 0.0
case .portraitUpsideDown:
rotation = CGFloat.pi
case .landscapeLeft:
rotation = -CGFloat.pi/2.0
case .landscapeRight:
rotation = CGFloat.pi/2.0
default:
break
}
let xScale = self.previewView.transform.xScale
let yScale = self.previewView.transform.yScale
self.previewView.transform = CGAffineTransform(scaleX:xScale, y:yScale).rotated(by:rotation)
self.view.layoutIfNeeded()
}
Here is the extension to CGAffineTransform the code above uses
extension CGAffineTransform {
public var xScale: CGFloat {
get {return sqrt(self.a * self.a + self.c * self.c) }
}
public var yScale: CGFloat {
get {return sqrt(self.b * self.b + self.d * self.d) }
}
public var rotation: CGFloat {
get {return CGFloat(atan2f(Float(self.b), Float(self.a))) }
}
}

Different size classes for iPad portrait and landscape modes with containerviews

I've found this question and I've tried to implement the solution that has been given. However I run into a problem.
My initial view controller has two container views who both have their own view controller. I've created a root view controller that is assigned to the initial view controller. The code in this class looks like this.
class RootViewController: UIViewController {
var willTransitionToPortrait: Bool!
var traitCollection_CompactRegular: UITraitCollection!
var traitCollection_AnyAny: UITraitCollection!
override func viewDidLoad() {
super.viewDidLoad()
setupReferenceSizeClasses()
// Do any additional setup after loading the view.
}
override func viewWillAppear(animated: Bool) {
willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
willTransitionToPortrait = size.height > size.width
}
func setupReferenceSizeClasses(){
let traitCollection_hCompact = UITraitCollection(horizontalSizeClass: .Compact)
let traitCollection_vRegular = UITraitCollection(verticalSizeClass: .Regular)
traitCollection_CompactRegular = UITraitCollection(traitsFromCollections: [traitCollection_hCompact, traitCollection_vRegular])
let traitCollection_hAny = UITraitCollection(horizontalSizeClass: .Unspecified)
let traitCollection_vAny = UITraitCollection(verticalSizeClass: .Unspecified)
traitCollection_AnyAny = UITraitCollection(traitsFromCollections: [traitCollection_hAny, traitCollection_vAny])
}
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection? {
let traitCollectionForOverride = ((willTransitionToPortrait) != nil) ? traitCollection_CompactRegular : traitCollection_AnyAny
return traitCollectionForOverride;
}
However when I run it the size class won't respons like it should. One of the container view controllers will start acting weird in both landscape and portrait mode like can be seen below.
When I don't assign the rootviewcontroller it will look like this
While it should look like this in portrait mode
Does anyone know what might be going wrong here? Why it doesn't change the size class like desired.
EDIT
Like #Muhammad Yawar Ali asked here are screenshots from the position of all the size classes I've set. I have no warnings or errors on any constraints so these screenshots contain the updated views.
I hope this shows everything that is needed.
EDIT:
for some reason I'm unable to put in all the screenshots
On the viewWillTransitionToSize you need to call also super, to pass the event to the next responder (your rootviewcontroller)...
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
willTransitionToPortrait = size.height > size.width
}
Realize this is over two years old but...
I just ran across what I think is a similar issue. What you may be forgetting is that 'overrideTraitCollectionForChildViewController' only overrides the views children, so this method won't do anything with the containers since they are located at the root.
I solved this putting my two containers in a UIStackView in Interface Builder and made a property of this stack in code and then updated the axis depending on the orientation. For example, in Objective-C:
#property (weak, nonatomic) IBOutlet UIStackView *rootStack;
// ...
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) {
return [super overrideTraitCollectionForChildViewController:childViewController];
}
if (CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds)) {
self.rootStack.axis = UILayoutConstraintAxisVertical;
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
}
else {
self.rootStack.axis = UILayoutConstraintAxisHorizontal;
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
If you have any constraints that are different between portrait and landscape you will need to adjust those in code as well.
I suppose you could also solve this by embedding the view controller with the containers in another view controller.
I have cloned your code from repository : https://github.com/MaikoHermans/sizeClasses.git
And editted code put the below code in you controller it will work fine & will not effect your design in iPads.
import UIKit
class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection? {
if view.bounds.width < view.bounds.height {
return UITraitCollection(horizontalSizeClass: .Unspecified)
} else {
return UITraitCollection(horizontalSizeClass: .Regular)
}
}
}
You can try with this code but There is an issue i believe its not updating traits properly for ipads and view layout remains same but looks good. I have tried multiple ways but not succeeded yet will update my answer.

Disable autorotate on a single subview in iOS8

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.

Resources