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

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.

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!

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.

Scaling current dot of UIPageControl and keeping it centered

I've subclassed UIPageControl in order to have its current dot bigger.
class CustomPageControl: UIPageControl {
override var currentPage: Int {
didSet {
updateDots()
}
}
func updateDots() {
let currentDot = subviews[currentPage]
let largeScaling = CGAffineTransform(scaleX: 3, y: 3)
subviews.forEach {
// apply the large scale of newly selected dot
// restore the normal scale of previously selected dot
$0.transform = $0 == currentDot ? largeScaling : .identity
}
}
}
But the result of the transform isn't centered (the red dot should be aligned with the others):
I've tried (on iOS 12):
changing the frame or center of currentDot has no effect.
changing the transform to include a translatedBy(x: CGFloat, y: CGFloat) has no effect.
changing the constraints like here is making the first dot jumping:
currentDot.translatesAutoresizingMaskIntoConstraints = false
currentDot.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 0)
currentDot.centerXAnchor.constraint(equalTo: self.centerXAnchor, constant: 0)
I got it finally working by rewriting all the subviews constraints by myself.
// https://stackoverflow.com/a/55063316/1033581
class DefaultPageControl: UIPageControl {
override var currentPage: Int {
didSet {
updateDots()
}
}
override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
super.sendAction(action, to: target, for: event)
updateDots()
}
private func updateDots() {
let currentDot = subviews[currentPage]
let largeScaling = CGAffineTransform(scaleX: 3.0, y: 3.0)
let smallScaling = CGAffineTransform(scaleX: 1.0, y: 1.0)
subviews.forEach {
// Apply the large scale of newly selected dot.
// Restore the small scale of previously selected dot.
$0.transform = $0 == currentDot ? largeScaling : smallScaling
}
}
override func updateConstraints() {
super.updateConstraints()
// We rewrite all the constraints
rewriteConstraints()
}
private func rewriteConstraints() {
let systemDotSize: CGFloat = 7.0
let systemDotDistance: CGFloat = 16.0
let halfCount = CGFloat(subviews.count) / 2
subviews.enumerated().forEach {
let dot = $0.element
dot.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.deactivate(dot.constraints)
NSLayoutConstraint.activate([
dot.widthAnchor.constraint(equalToConstant: systemDotSize),
dot.heightAnchor.constraint(equalToConstant: systemDotSize),
dot.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 0),
dot.centerXAnchor.constraint(equalTo: centerXAnchor, constant: systemDotDistance * (CGFloat($0.offset) - halfCount))
])
}
}
}
System constants in the code (7.0 and 16.0) are respectively the size and the distance found for a default UIPageControl dot on iOS 12.
I tried the solution proposed by Cœur in Swift 5 and Xcode 11 and it works fine with a few notes:
The PageControl element in IB/Storyboard has to be positioned with constraints.
The dots are slightly off-center but it can be quickly fixed by changing the constant of the last constraint to systemDotDistance * ( CGFloat($0.offset) - (halfCount - 0.5)).
If the updateConstraints override is never called, you might need to call self.view.setNeedsUpdateConstraints() in the view controller.

How to make UIView's width increase when a upload percentage increases? SWIFT 4

I have a project that uploads multiple images to Firebase, and have made a constant that calculates the percentage of the current upload progress:
uploadTask.observe(.progress, handler: { (snapshot) in
guard let progress = snapshot.progress else {
return
}
let percentage = (Float(progress.completedUnitCount) / Float(progress.totalUnitCount))
progressBlock(Double(percentage))
})
I have made a UIView element but have not connected it. I am trying to make the UIView acts as a progress bar for the user to visibly see how the upload is progressing. I have been trying to do this but have been unsuccessful at it. How can I do this? By the way: The UIView should be increasing as the percentage increases, and at 100% the UIView will reset and hide.
Thank you so much!
Create a custom subclass of UIView. Give the custom subclass a property percentComplete.
Have the view use a CAShapeLayer to draw a filled area that expands to fill the view as the percentage value increases from 0.0 to 1.0. (You could also override the drawRect() method and use Quartz drawing, but shape layers are easier and more performant.
Do some searching on CAShapeLayer for ideas on how to do that.
You can either have your view add a shape layer as an additional layer on the view, or you can have your view provide a layerClass property that causes the view's content layer to be a single CAShapeLayer
Here is the code to change width of your view dynamically.
import UIKit
class ViewController: UIViewController {
var widthAnchor:NSLayoutConstraint!
let perecentageView:UIView={
let view = UIView()
view.backgroundColor = .red
view.translatesAutoresizingMaskIntoConstraints = false//Its needed becasue we are using anchors
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(perecentageView)
perecentageView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100).isActive = true
perecentageView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 10).isActive = true
perecentageView.heightAnchor.constraint(equalToConstant: 50).isActive = true
widthAnchor = perecentageView.widthAnchor.constraint(equalToConstant: 0) // By default its 0
widthAnchor.isActive = true
self.uploadProgess()
}
func uploadProgess(){
uploadTask.observe(.progress, handler: { (snapshot) in
guard let progress = snapshot.progress else {
return
}
let percentage = (Float(progress.completedUnitCount) / Float(progress.totalUnitCount))
let newWidth = self.view.frame.width * percentage / 100
DispatchQueue.main.async(execute: {
self.widthAnchor.constant = newWidth
// self.view.frame.width is the parent view (And max width will be = self.view.frame.width)
//if you are using view from storybard with aoutolayout then just take IBOutlet of view's with and here widthAnchor = your IBOutlet
/*
if you are using view from storybard and without autolayout then use below code
self.perecentageView.frame = CGRect(x: self.perecentageView.frame.origin.x, y: self.perecentageView.frame.origin.y, width: newWidth, height: self.perecentageView.frame.height)
*/
})
})
}
}

Cannot fix Auto Layout animation while rotation event

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)
}

Resources