Cannot fix Auto Layout animation while rotation event - ios

Don't be afraid of the huge code that will follow here. You can copy and paste the code snippet into a new single view application to see how it behaves. The problem sits somewhere inside the completion block of the animation executed alongside the rotation animation.
import UIKit
let sizeConstant: CGFloat = 60
class ViewController: UIViewController {
let topView = UIView()
let backgroundView = UIView()
let stackView = UIStackView()
let lLayoutGuide = UILayoutGuide()
let bLayoutGuide = UILayoutGuide()
var bottomConstraints = [NSLayoutConstraint]()
var leftConstraints = [NSLayoutConstraint]()
var bLayoutHeightConstraint: NSLayoutConstraint!
var lLayoutWidthConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
print(UIScreen.main.bounds)
// self.view.layer.masksToBounds = true
let views = [
UIButton(type: .infoDark),
UIButton(type: .contactAdd),
UIButton(type: .detailDisclosure)
]
views.forEach(self.stackView.addArrangedSubview)
self.backgroundView.backgroundColor = UIColor.red
self.backgroundView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(self.backgroundView)
self.topView.backgroundColor = UIColor.green
self.topView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(self.topView)
self.stackView.axis = isPortrait() ? .horizontal : .vertical
self.stackView.distribution = .fillEqually
self.stackView.translatesAutoresizingMaskIntoConstraints = false
self.backgroundView.addSubview(self.stackView)
self.topView.topAnchor.constraint(equalTo: self.topLayoutGuide.bottomAnchor).isActive = true
self.topView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
self.topView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
self.topView.heightAnchor.constraint(equalToConstant: 46).isActive = true
self.view.addLayoutGuide(self.lLayoutGuide)
self.view.addLayoutGuide(self.bLayoutGuide)
self.bLayoutGuide.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
self.bLayoutGuide.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
self.bLayoutGuide.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
self.bLayoutHeightConstraint = self.bLayoutGuide.heightAnchor.constraint(equalToConstant: isPortrait() ? sizeConstant : 0)
self.bLayoutHeightConstraint.isActive = true
self.lLayoutGuide.topAnchor.constraint(equalTo: self.topView.bottomAnchor).isActive = true
self.lLayoutGuide.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
self.lLayoutGuide.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
self.lLayoutWidthConstraint = self.lLayoutGuide.widthAnchor.constraint(equalToConstant: isPortrait() ? 0 : sizeConstant)
self.lLayoutWidthConstraint.isActive = true
self.stackView.topAnchor.constraint(equalTo: self.backgroundView.topAnchor).isActive = true
self.stackView.bottomAnchor.constraint(equalTo: self.backgroundView.bottomAnchor).isActive = true
self.stackView.leadingAnchor.constraint(equalTo: self.backgroundView.leadingAnchor).isActive = true
self.stackView.trailingAnchor.constraint(equalTo: self.backgroundView.trailingAnchor).isActive = true
self.bottomConstraints = [
self.backgroundView.topAnchor.constraint(equalTo: self.bLayoutGuide.topAnchor),
self.backgroundView.leadingAnchor.constraint(equalTo: self.bLayoutGuide.leadingAnchor),
self.backgroundView.trailingAnchor.constraint(equalTo: self.bLayoutGuide.trailingAnchor),
self.backgroundView.heightAnchor.constraint(equalToConstant: sizeConstant)
]
self.leftConstraints = [
self.backgroundView.topAnchor.constraint(equalTo: self.lLayoutGuide.topAnchor),
self.backgroundView.bottomAnchor.constraint(equalTo: self.lLayoutGuide.bottomAnchor),
self.backgroundView.trailingAnchor.constraint(equalTo: self.lLayoutGuide.trailingAnchor),
self.backgroundView.widthAnchor.constraint(equalToConstant: sizeConstant)
]
if isPortrait() {
NSLayoutConstraint.activate(self.bottomConstraints)
} else {
NSLayoutConstraint.activate(self.leftConstraints)
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
let willBePortrait = size.width < size.height
coordinator.animate(alongsideTransition: {
context in
let halfDuration = context.transitionDuration / 2.0
UIView.animate(withDuration: halfDuration, delay: 0, options: .overrideInheritedDuration, animations: {
self.bLayoutHeightConstraint.constant = 0
self.lLayoutWidthConstraint.constant = 0
self.view.layoutIfNeeded()
}, completion: {
_ in
// HERE IS THE ISSUE!
// Putting this inside `performWithoutAnimation` did not helped
if willBePortrait {
self.stackView.axis = .horizontal
NSLayoutConstraint.deactivate(self.leftConstraints)
NSLayoutConstraint.activate(self.bottomConstraints)
} else {
self.stackView.axis = .vertical
NSLayoutConstraint.deactivate(self.bottomConstraints)
NSLayoutConstraint.activate(self.leftConstraints)
}
self.view.layoutIfNeeded()
UIView.animate(withDuration: halfDuration) {
if willBePortrait {
self.bLayoutHeightConstraint.constant = sizeConstant
} else {
self.lLayoutWidthConstraint.constant = sizeConstant
}
self.view.layoutIfNeeded()
}
})
})
super.viewWillTransition(to: size, with: coordinator)
}
func isPortrait() -> Bool {
let size = UIScreen.main.bounds.size
return size.width < size.height
}
}
Here are a few screenshots of the issue I'm unable to solve. Look closely at the corners:
I'd assume that after reactivating different constraint array and force recalculation, the view would immediately snap to the layout guide, but as shown, it doesn't. Furthermore I don't understand why the red view is not in sync with the stack view, even if the stackview should always follow it's superview, which here is the red view.
PS: The best way to test it is the iPhone X Plus simulator.

Use Size Classes
An entirely different approach to smoothly animate the toolbar animation is to leverage on the autoLayout size classes, specifically hR (height Regular) and hC (height Compact), and create different constraints for each.
↻ replay animation
A further improvement is to actually use two distinct toolbars, one for the vertical display, and one for the horizontal one. This is not by any mean a requirement, but it solves the resizing of the toolbar itself (†).
A final refinement is to implement these changes in Interface Builder, yielding exactly 0 lines of code, which of course is not mandatory either.
↻ replay animation
Zero Lines of Code
None of the proposed solutions tinker with UIViewControllerTransitionCoordinator which not only greatly simplify the source code development and maintenance, it also doesn't need to rely on hardcoded values or supporting utilities. You also get a preview in Interface Builder. And once finalized in IB, you can still convert the logic to runtime programming if it is an absolute requirement.
Notice that the UIStackView is embedded in the toolbar, and thus follows the animation. You can control the amount of swing of the toolbars out of sight by a constant ; I picked 1024 so that they move quickly out of the screen, and only reappear at the end of the transition.
(†) Further leveraging on Interface Builder and size classes, you may still use a single toolbar, but if you do so it will resize during the transition. Again, the UIStackView is embedded, and its orientation is, too, size classes dependent, and the OS handles all the animation without the need to create a coordinator:
► Find this solution on GitHub and additional details on Swift Recipes.

I'd assume that after reactivating different constraint array and force recalculation, the view would immediately snap to the layout guide, but as shown, it doesn't.
This isn't happening because you are activating/deactivating your constraints inside of the coordinator.animate(alongsideTransition:completion:) method. When you use this method, everything inside of the animation block will be animated along with your view controller's transition, so there will be no immediate snaps to the layout guide. If you wanted the red view and green view to immediately snap to their new positions before the rotation animation, then animate to fill their desired positions as the view controller is animating, then you could do something like this:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
let willBePortrait = size.width < size.height
self.bLayoutHeightConstraint.constant = 0
self.lLayoutWidthConstraint.constant = 0
if willBePortrait {
self.stackView.axis = .horizontal
NSLayoutConstraint.deactivate(self.leftConstraints)
NSLayoutConstraint.activate(self.bottomConstraints)
} else {
self.stackView.axis = .vertical
NSLayoutConstraint.deactivate(self.bottomConstraints)
NSLayoutConstraint.activate(self.leftConstraints)
}
self.view.layoutIfNeeded()
coordinator.animate(alongsideTransition: { context in
let halfDuration = context.transitionDuration / 2
UIView.animate(withDuration: halfDuration, delay: halfDuration, animations: {
if willBePortrait {
self.bLayoutHeightConstraint.constant = sizeConstant
} else {
self.lLayoutWidthConstraint.constant = sizeConstant
}
self.view.layoutIfNeeded()
})
})
super.viewWillTransition(to: size, with: coordinator)
}
EDIT: If you want to animate the red view out while transitioning, and then animate the red view back in at the end, then that's a good time to use the coordinator, as the animation out can happen in the animation block and you can split the out- and in- blocks inside of it:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
let willBePortrait = size.width < size.height
if willBePortrait {
self.stackView.axis = .horizontal
NSLayoutConstraint.deactivate(self.leftConstraints)
NSLayoutConstraint.activate(self.bottomConstraints)
} else {
self.stackView.axis = .vertical
NSLayoutConstraint.deactivate(self.bottomConstraints)
NSLayoutConstraint.activate(self.leftConstraints)
}
self.view.layoutIfNeeded()
coordinator.animate(alongsideTransition: { context in
let halfDuration = context.transitionDuration / 2
UIView.animate(withDuration: halfDuration, animations: {
if willBePortrait {
self.lLayoutWidthConstraint.constant = 0
} else {
self.bLayoutHeightConstraint.constant = 0
}
})
UIView.animate(withDuration: halfDuration, delay: halfDuration, animations: {
if willBePortrait {
self.bLayoutHeightConstraint.constant = sizeConstant
} else {
self.lLayoutWidthConstraint.constant = sizeConstant
}
self.view.layoutIfNeeded()
})
})
super.viewWillTransition(to: size, with: coordinator)
}

Related

How to Apply Animation while changing UIView size? + Swift 5

Im new to IOS development and I have two buttons in the UIView and when user select the option portrait or landscape, change the UIView re-size and change the background color as well and i need to add animation for that process.
As as ex:
User select portrait and then user can see red color UIVIew. after click the landscape option, animation should be started and it looks like, red color image come front and change the size (changing height and width) for landscape mode and go to previous position and change color to green. i have added small UIView animation on code and it is helpful to you identify the where should we start the animation and finish it. if someone know how to it properly, please, let me know and appreciate your help. please, refer below code
import UIKit
class ViewController: UIViewController {
let portraitWidth : CGFloat = 400
let portraitHeight : CGFloat = 500
let landscapeWidth : CGFloat = 700
let landscapeHeight : CGFloat = 400
var mainView: UIView!
var mainStackView: UIStackView!
let segment: UISegmentedControl = {
let segementControl = UISegmentedControl()
return segementControl
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.translatesAutoresizingMaskIntoConstraints = false
mainStackView = UIStackView()
mainStackView.axis = .vertical
mainStackView.translatesAutoresizingMaskIntoConstraints = false
mainStackView.alignment = .center
mainStackView.distribution = .equalCentering
self.view.addSubview(mainStackView)
self.segment.insertSegment(withTitle: "Portrait", at: 0, animated: false)
self.segment.insertSegment(withTitle: "Landscape", at: 1, animated: false)
self.segment.selectedSegmentIndex = 0
self.segment.addTarget(self, action: #selector(changeOrientation(_:)), for: .valueChanged)
self.segment.translatesAutoresizingMaskIntoConstraints = false
self.segment.selectedSegmentIndex = 0
mainStackView.addArrangedSubview(self.segment)
let safeAreaLayoutGuide = self.view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
self.mainStackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20),
self.mainStackView.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor, constant: 0),
])
NSLayoutConstraint.activate([
self.segment.heightAnchor.constraint(equalToConstant: 35),
self.segment.widthAnchor.constraint(equalToConstant: 300)
])
mainView = UIView(frame: .zero)
mainView.translatesAutoresizingMaskIntoConstraints = false
mainStackView.addArrangedSubview(mainView)
mainView.backgroundColor = UIColor.red
NSLayoutConstraint.activate([
mainView.heightAnchor.constraint(equalToConstant: portraitHeight),
mainView.widthAnchor.constraint(equalToConstant: portraitWidth),
mainView.topAnchor.constraint(equalTo: segment.bottomAnchor, constant: 30)
])
}
#IBAction func changeOrientation(_ sender: UISegmentedControl) {
self.mainView.constraints.forEach{ (constraint) in
self.mainView.removeConstraint(constraint)
}
UIView.animate(withDuration: 1.0) {
if (sender.selectedSegmentIndex == 0) {
self.mainView.backgroundColor = UIColor.red
NSLayoutConstraint.activate([
self.mainView.heightAnchor.constraint(equalToConstant: self.portraitHeight),
self.mainView.widthAnchor.constraint(equalToConstant: self.portraitWidth),
self.mainView.topAnchor.constraint(equalTo: self.segment.bottomAnchor, constant: 30)
])
} else {
self.mainView.backgroundColor = UIColor.green
NSLayoutConstraint.activate([
self.mainView.heightAnchor.constraint(equalToConstant: self.landscapeHeight),
self.mainView.widthAnchor.constraint(equalToConstant: self.landscapeWidth),
self.mainView.topAnchor.constraint(equalTo: self.segment.bottomAnchor, constant: 30)
])
}
}
}
}
updated logic
#IBAction func changeOrientation(_ sender: UISegmentedControl) {
UIView.animate(withDuration: 4.0) {
if (sender.selectedSegmentIndex == 0) {
self.mainView.backgroundColor = UIColor.red
self.widthConstraint.constant = self.portraitWidth
self.heightConstraint.constant = self.portraitWidth
} else {
self.mainView.backgroundColor = UIColor.green
self.widthConstraint.constant = self.landscapeWidth
self.heightConstraint.constant = self.landscapeHeight
}
self.mainView.layoutIfNeeded()
} }
It's possible, the basic problem is:
• It's not possible to animate an actual change between one constraint and another.
To change size or shape you simply animate the length of a constraint.
While you are learning I would truly urge you to simply animate the constraints.
So, don't try to "change" constraints, simply animate the length of one or the other.
yourConstraint.constant = 99 // it's that easy
I also truly urge you to just lay it out on storyboard.
You must master that first!
Also there is no reason at all for the stack view, get rid of it for now.
Just have an outlet
#IBOutlet var yourConstraint: NSLayoutConstraint!
and animate
UIView.animateWithDuration(4.0) {
self.yourConstraint.constant = 666
self.view.layoutIfNeeded()
}
Be sure to search in Stack Overflow for "iOS animate constraints" as there is a great deal to learn.
Do this on storyboard, it will take 20 seconds tops for such a simple thing. Once you have mastered storyboard, only then move on to code if there is some reason to do so
Forget the stack view
Simply animate the constant of the constraint(s) to change the size, shape or anything you like.
Be sure to google for the many great articles "animating constraints in iOS" both here on SO and other sites!

Not able to lay out Subviews on a custom UIView in Swift

So I have created this custom container view which I am laying out using autolayout constraint.
func configureSegmentContainerView() {
segmentContainerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 40).isActive = true
segmentContainerView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8).isActive = true
segmentContainerView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -8).isActive = true
segmentContainerView.heightAnchor.constraint(equalToConstant: 3).isActive = true
}
In the view controller, the viewDidLoad() is this:
setupDataSource()
segmentContainerView = ATCStorySegmentsView()
view.addSubview(segmentContainerView)
configureSegmentContainerView()
segmentContainerView.translatesAutoresizingMaskIntoConstraints = false
segmentContainerView.numberOfSegments = friendStory.count
Now once the data source is setup and I have the friendStory count, I am assigning it to segmentContainerView.numberofSegments
In segmentContainerview class this is what is happening:
var numberOfSegments: Int? {
didSet {
addSegments()
}
}
In addSegments(), I am adding UIViews depending upon the numberOfSegments this is the code for that:
private func addSegments() {
guard let numberOfSegments = numberOfSegments else { return }
layoutIfNeeded()
setNeedsLayout()
for i in 0..<numberOfSegments {
let segment = Segment()
addSubview(segment.bottomSegment)
addSubview(segment.topSegment)
configureSegmentFrame(index: i, segmentView: segment)
segmentsArray.append(segment)
}
}
private func configureSegmentFrame(index: Int, segmentView: Segment) {
guard let numberOfSegments = numberOfSegments else { return }
let widthOfSegment : CGFloat = (self.frame.width - (padding * CGFloat(numberOfSegments - 1))) / CGFloat(numberOfSegments)
let i = CGFloat(index)
let segmentFrame = CGRect(x: i * (widthOfSegment + padding), y: 0, width: widthOfSegment, height: self.frame.height)
segmentView.bottomSegment.frame = segmentFrame
segmentView.topSegment.frame = segmentFrame
segmentView.topSegment.frame.size.width = 0
}
**Question and Issue: ** Instead of getting 4 UIViews, I am getting 3, but the third one is not correctly placed and is going outside the parent container. How can I get these uiviews aligned correctly. I am guessing there is some issue with where setNeedsLayout() and layoutIfNeeded() needs to be called. Please help.
Segment is a struct with two properties - bottomSegment and topSegment. Both being UIView
You can see how just three UIView segments appear. I needs to 4 (numberOfSegments = 4) of these. Also I am giving the parent container constant of 8 and -8 for right and leftAnchor. so all 4 segments need to be placed within this view. As you can see in the picture above the last segment is going outside of the parent container.
Try to call addSegments at onViewDidAppear. If it works, it means that in your code the view does not still have the correct frame.
For this to work you need to adapt the frames of the views, when the view controller's view changed:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let numberOfSegments = numberOfSegments else { return }
for i in 0..<numberOfSegments {
configureSegmentFrame(index: i, segmentView: segment)
}
}
You will probably have to do it cleaner, but it should work.
The problem is that
let widthOfSegment : CGFloat = (self.frame.width ...
is being called too soon. You cannot do anything in that depends upon self having its frame until after it has its real frame.
For a UIView, that moment is after layoutSubviews has been called for the first time.

UICollectionViewController with dynamic header (like Reddit App)

I am looking for a way to implement a header view that automatically hides once you start scrolling down and immediately shows itself once the user starts scrolling up.
Usually, I always post some code, but now I am a little bit lost on how to implement such behaviour.
My view layout:
UICollectionViewController with paging enabled for horizontal
scrolling (has two items)
The UICollectionViewCell fills the entire vertical space. Each UICollectionViewCell hosts a UITableView for vertical scrolling. I assume that I have to use the UITableView vertical scrolling position to adjust the frame of the menu bar.
Video: https://imgur.com/a/Rdu3wko
What would be the best way to implement such a behaviour?
If you want to use a UICollectionView, just grab the delegate, see which direction the user is scrolling, and hide/show the header as needed. Here's an example to get you started:
class ViewController: UIViewController {
// Variable to save the last scroll offset.
private var lastContentOffset: CGFloat = 0
private lazy var header: UIView = {
let header = UIView()
header.translatesAutoresizingMaskIntoConstraints = false
header.backgroundColor = .red
header.widthAnchor.constraint(equalToConstant: self.view.frame.width).isActive = true
header.heightAnchor.constraint(equalToConstant: 80.0).isActive = true
return header
}()
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout())
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
collectionView.backgroundColor = .white
collectionView.contentSize = CGSize(width: UIScreen.main.bounds.width, height: 2000.0)
// Setting bounces to false - otherwise the header will disappear when we go past the top and are sprung back.
collectionView.bounces = false
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(collectionView)
collectionView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
collectionView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
collectionView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
collectionView.contentSize = CGSize(width: UIScreen.main.bounds.width, height: 2000.0)
// Make sure you either add the header subview last, or call self.view.bringSubviewToFront(header)
self.view.addSubview(header)
// Constrain the header so it's just sitting on top of the view. To make it visible, we'll use a transform.
header.bottomAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
header.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
// Header starts visible.
header.layoutIfNeeded()
self.header.transform = CGAffineTransform(translationX: 0.0, y: header.frame.height)
}
func revealHeader() {
// Set the duration below to how quickly you want header to appear/disappear.
UIView.animate(withDuration: 0.3) {
self.header.transform = CGAffineTransform(translationX: 0.0, y: self.header.frame.height)
}
}
func hideHeader() {
UIView.animate(withDuration: 0.3) {
self.header.transform = .identity
}
}
}
extension ViewController: UICollectionViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (lastContentOffset > scrollView.contentOffset.y) {
// Scrolled up: reveal header.
revealHeader()
}
else if (lastContentOffset < scrollView.contentOffset.y) {
// Scrolled down: reveal header.
hideHeader()
}
lastContentOffset = scrollView.contentOffset.y
}
}
EDIT: Noticed the functionality of the Reddit header is a bit different. If you want the thing to scroll dynamically (i.e. by the amount you have scrolled down by as opposed to appear all at once) replace that delegate function with this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (lastContentOffset > scrollView.contentOffset.y) {
// Scrolled up: reveal header.
let difference = lastContentOffset - scrollView.contentOffset.y
if header.transform.ty < (header.frame.height - difference) {
// Header hasn't been fully revealed yet, bring it down by the amount we've scrolled up.
self.header.transform = CGAffineTransform(translationX: 0.0, y: header.transform.ty + difference)
} else {
self.header.transform = CGAffineTransform(translationX: 0.0, y: header.frame.height)
}
}
else if (lastContentOffset < scrollView.contentOffset.y) {
// Scrolled down: reveal header.
let difference = scrollView.contentOffset.y - lastContentOffset
if header.transform.ty > difference {
self.header.transform = CGAffineTransform(translationX: 0.0, y: header.transform.ty - difference)
} else {
self.header.transform = CGAffineTransform(translationX: 0.0, y: 0.0)
}
}
lastContentOffset = scrollView.contentOffset.y
}
This functionality is possible in UITableView set parallax header otherwise UIScrollView parallax animation.

How to get safe area frame of programmatically created ViewController?

Note : Not created ViewController in storyboard
I created ViewController programmatically after giving support for safe guard now my problem is that how i can got his height?
I try following thing but no luck
let guide = view.safeAreaLayoutGuide
let height = guide.layoutFrame.size.height
print("\(height)")
print("\(self.view.frame.height)")
height = 812.0, self.view height = 812.0
Is there any other way?
A safeAreaLayoutGuide, which is iOS 11+, is meant for auto layout and to try to get a frame is close to trying to make orange juice from a lemon.
If what you want is to find out is what you can safely use for a frame, maybe try something like this:
Create a transparent view:
let mySafeView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
mySafeView.backgroundColor = UIColor.clear
mySafeView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mySafeView)
// if needed, send this view to the back of the hierarchy
view.sendSubview(toBack: mySafeView)
}
Next, in your constraint-building code attach it to the true anchors:
// create generic layout guides
let layoutGuideTop = UILayoutGuide()
view.addLayoutGuide(layoutGuideTop)
let layoutGuideBottom = UILayoutGuide()
view.addLayoutGuide(layoutGuideBottom)
let margins = view.layoutMarginsGuide
view.addLayoutGuide(margins)
// get correct top/bottom margins based on iOS version
if #available(iOS 11, *) {
let guide = view.safeAreaLayoutGuide
layoutGuideTop.topAnchor.constraintEqualToSystemSpacingBelow(guide.topAnchor, multiplier: 1.0).isActive = true
layoutGuideBottom.bottomAnchor.constraintEqualToSystemSpacingBelow(guide.bottomAnchor, multiplier: 1.0).isActive = true
} else {
layoutGuideTop.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true
layoutGuideBottom.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
}
mySafeView.topAnchor.constraint(equalTo: layoutGuideTop.topAnchor).isActive = true
mySafeView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
mySafeView.bottomAnchor.constraint(equalTo: layoutGuideBottom.bottomAnchor).isActive = true
mySafeView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
Finally, get the frame size:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(mySafeView.frame)
}
On an iPhone 6 plus running iOS 11.1 you get:
(0.0, 28.0, 414.0, 716.0)
On an iPhone 6 plus running iOS 9.0 you get:
(0.0, 20.0, 414.0, 716.0)
First off you can't do it in the viewDidLoad. You have to wait till viewDidLayoutSubviews
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myGlobalVars.sceneRect = view.frame
if #available(iOS 11.0, *) {
myGlobalVars.topSafeArea = view.safeAreaInsets.top
myGlobalVars.bottomSafeArea = view.safeAreaInsets.bottom
} else {
myGlobalVars.topSafeArea = topLayoutGuide.length
myGlobalVars.bottomSafeArea = bottomLayoutGuide.length
}
}

Custom UINavigationBar - Animating Intrinsic Height Changes

In this particular scenario, I'm actually very confused. I have subclassed UINavigationBar, noted Apple's outdated sample code, realized that sizeToFit is never called and overrode intrinsicContentSize and layoutSubviews to calculate the necessary height. Everything works well when it comes to calculating the height based on a custom view I provide (I use systemLayoutSizeFittingSize for calculating the height dynamically based on a custom content view/titleView).
The odd part is, when I attempt to animate any changes, I can animate changes in the custom titleView, but when reloading the intrinsicContentSize via invalidateIntrinsicContentSize(), the view frame changes do not animate. It snaps.
Sample code:
let delta = destinationTextFieldContainerView.frame.minX - originTextFieldContainerView.frame.minX + originTextFieldContainerView.frame.height
textContainersSeparatorConstraint.constant = -delta
UIView.animateWithDuration(2, delay: 2, options: UIViewAnimationOptions.CurveEaseIn, animations: {
self.customContainerNavigationBar.invalidateIntrinsicContentSize()
self.customContainerNavigationBar.layoutIfNeeded()
}) { (_) in
self.customContainerNavigationBar.invalidateIntrinsicContentSize()
}
The one constraint value change will animate, but unfortunately (because navBars don't rely on auto layout) it seems there's a problem updating the frame. I've attempted updating the frame directly before invalidating the intrinsic size with no success. I've also tried a height constraint, listing it in key frame animations with different timing offsets, invalidating the layout at different points in time, all to no success. Any help would be greatly appreciated.
Resizing code can be found here:
override func intrinsicContentSize() -> CGSize {
var navigationBarSize = super.intrinsicContentSize()
navigationBarSize.width = superview?.frame.width ?? 0
guard let titleView = navigationItem?.titleView else {
return navigationBarSize
}
let fittingSize = CGSize(width: navigationBarSize.width, height: UILayoutFittingCompressedSize.height)
let titleViewSize = titleView.systemLayoutSizeFittingSize(fittingSize)
navigationBarSize.height = max(titleViewSize.height, 44)
navigationBarSize.width = UIViewNoIntrinsicMetric
return navigationBarSize
}
override func layoutSubviews() {
super.layoutSubviews()
guard let navigationItem = navigationItem, titleView = navigationItem.titleView else {
return
}
var navigationBarSize = bounds.size
navigationBarSize.width = superview?.frame.width ?? 0
var fittingSize = CGSize(width: navigationBarSize.width, height: UILayoutFittingCompressedSize.height)
if let leftBarButtonItem = navigationItem.leftBarButtonItem, leftButtonCustomView = leftBarButtonItem.customView {
fittingSize.width -= leftButtonCustomView.frame.width + leftButtonCustomView.frame.origin.x + 22
}
let titleViewSize = titleView.systemLayoutSizeFittingSize(fittingSize, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow)
titleView.frame = CGRect(origin: CGPoint(x: titleView.frame.origin.x, y: 0), size: titleViewSize)
titleView.setNeedsLayout()
titleView.layoutIfNeeded()
if let leftBarButtonItem = navigationItem.leftBarButtonItem, leftButtonCustomView = leftBarButtonItem.customView {
var customViewFrame = CGRect(origin: CGPoint.zero, size: navigationBarSize)
let customViewSize = leftButtonCustomView.systemLayoutSizeFittingSize(customViewFrame.size, withHorizontalFittingPriority: UILayoutPriorityDefaultLow, verticalFittingPriority: UILayoutPriorityDefaultLow)
customViewFrame.size = customViewSize
leftButtonCustomView.frame = customViewFrame
leftButtonCustomView.setNeedsLayout()
leftButtonCustomView.layoutIfNeeded()
}
invalidateIntrinsicContentSize()
}

Resources