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

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!

Related

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.

How to change WIDTH of UITableView with animation?

I have UITableView on the left size and plain UIView on the right side in my UIViewController
UITableView connects to .top, .leading and .bottom of
superview
UIView connects to .top, .trailing and .bottom of
superview, also it has .width
And UITableView .leading ==
.trailing of UIView
All these constraints you can see on the screenshot:
Here is my animation code in the controller, as you can see I update .width of UIView and also I cycle the animation.
class ViewController: UIViewController {
#IBOutlet var widthConstaint: NSLayoutConstraint!
#IBOutlet var tableView: UITableView!
#IBOutlet var watchContainerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
start()
}
func start() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if self.widthConstaint.constant == 50 {
self.change(width: 100)
} else {
self.change(width: 50)
}
self.start()
}
}
func change(width: CGFloat) {
widthConstaint.constant = width
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 300
}
}
As a result you can see this on .gif, what I should for smooth animation, without this glitch?
UPDATE
I tried to reproduce the UILabel wrong behavior with my own UIView subclass:
class MyView: UIView {
var currentSize: CGSize = .zero
override func layoutSubviews() {
super.layoutSubviews()
guard currentSize != bounds.size else { return }
currentSize = bounds.size
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
UIColor.purple.setFill()
UIBezierPath(rect: rect).fill()
let square = CGRect(x: rect.width - 50, y: rect.origin.y, width: 50, height: rect.height)
UIColor.blue.setFill()
UIBezierPath(rect: square).fill()
}
}
The view draws a little blue rectangle at its right, as a text. And… well… the animation is also good!
But if I change the contentMode to left (the default value is scaleToFill):
The blue square jumps from one place to the other.
So, I found out this is not only related to the UILabel's textAlignment. It's its combinaison with the default left content mode of the UILabel.
contentMode defines how the content should be adjusted if the content's size and the bounds size are different - this is case during the animation, the view redraws itself with its new size right before being resized.
So
label.contentMode = .left // default
label.textAlignment = .left
will behave as:
label.contentMode = .right
label.textAlignment = .right
PREVIOUS ANSWER
I spent some time on your problem. Guess what?
It's not related to the table view at all. It's because of the .right text alignment of the label.
You can reproduce the glitch with a simple UILabel:
class ViewController: UIViewController {
let label = UILabel()
var rightConstraint: NSLayoutConstraint!
override func loadView() {
view = UIView()
view.backgroundColor = .white
}
override func viewDidLoad() {
super.viewDidLoad()
setUpView()
start()
}
func start() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if self.rightConstraint.constant == 50 {
self.change(width: 100)
} else {
self.change(width: 50)
}
self.start()
}
}
func change(width: CGFloat) {
rightConstraint.constant = width
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
func setUpView() {
label.text = "Label"
label.textAlignment = .right
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
rightConstraint = view.trailingAnchor.constraint(equalTo: label.trailingAnchor)
rightConstraint.isActive = true
label.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
}
}
Why?
To find out the error, I created a UILabel subclass and overrode action(for: CALayer, forKey: String). It's where each UIView decides which animation to apply in response to a property change (ref). I was looking for some weird animation behaviors when a label is inside a table view.
If you print key, you'd see that actions are requested for the position, the bounds and the contents of the label when UIKit is about to commit the animations related to your UIView.animate(withDuration:) call.
In fact, setNeedsDisplay is called each time the size of an UILabel is changed (it makes sens). So I guess there is a conflict between the content redrawing of the label and its frame change.
I think you should recreate the right text alignment of the label without using textAlignment = .right.
Horizontally, pin the label to the right only. When layoutIfNeeded will be called, only the label's position will change, not its content.
As mentioned in the #Gaétanz answer. The abnormal behaviour is due to the Lable inside the cell.
You can remove the glitches by changing the Labels leading constraint relation to >= and change the priority to 999 from 1000.
Please refer the screenshot attached for your reference.
When you add constraint with a priority less than 1000 it will be an optional constraint. Priority 1000 is the required priority for a constraint. Here if you added a priority of 999, all the other constraints with priority 1000 will layout first and the less priority constraint will layout last.
You are changing the width of your side view outside of the animation block. put you widthConstaint.constant = width inside your animation block.
func change(width: CGFloat) {
UIView.animate(withDuration: 1.0) {
self.widthConstaint.constant = width
self.view.layoutIfNeeded()
}
}
According to your code the View is animated but the constraint is not put in the UIView animation block. So it looks like a glitch cause it changes the frame directly. Please try to change the code as below and try.
func change(width: CGFloat) {
UIView.animate(withDuration: 1.0) {
widthConstaint.constant = width
self.tableView.layoutIfNeeded()
self.view.layoutIfNeeded()
}
}

adding multiple image views by code

am trying to add image views while looping in an array.. like this:
let v = [1,2,3]
w = self.playgroundimg.frame.width/3 - 48.89/2
h = self.playgroundimg.frame.height/4 - 17
for x in v {
let imageName3 = "lineupcardbg"
let image3 = UIImage(named: imageName3)
let imageView3 = UIImageView(image: image3!)
imageView3.frame = CGRect(x: w, y: h, width: 48.89, height: 70.15)
self.playgroundimg.addSubview(imageView3)
w = w + self.playgroundimg.frame.width/3 - 48.89 - 15
}
this is working fine, but the dimension of the imageview will be different from device to device .. like this:
iphonex:
iphone 7 plus:
as you can see the one in the middle is not centered to the one below it .. how to achieve this so the middle is in the center and the other two are beside it with some space? and be displayed the same in all sizes?
playgroundimg constraints:
Here is a simple example - just to show how to use constraints from code.
You can paste this into a new playground page and run it to see the results.
Change the line:
get { return CGSize(width: 400, height: 400) }
to different sizes to see that the views remain centered.
import PlaygroundSupport
import UIKit
class RoundedImageView: UIImageView {
override init(frame: CGRect) {
super.init(frame: frame)
layer.cornerRadius = 8
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class AlignViewController: UIViewController {
override public var preferredContentSize: CGSize {
get { return CGSize(width: 400, height: 400) }
set { super.preferredContentSize = newValue }
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
var theImageViews = [UIImageView]()
for _ in 0..<4 {
let imgView = RoundedImageView(frame: CGRect.zero)
imgView.translatesAutoresizingMaskIntoConstraints = false
imgView.backgroundColor = .white
view.addSubview(imgView)
// make all views the same size and width
NSLayoutConstraint.activate([
imgView.widthAnchor.constraint(equalToConstant: 48.89),
imgView.heightAnchor.constraint(equalToConstant: 70.15),
])
theImageViews.append(imgView)
}
// for easy identification of which views we're referencing
let leftView = theImageViews[0]
let centerView = theImageViews[1]
let rightView = theImageViews[2]
let bottomView = theImageViews[3]
NSLayoutConstraint.activate([
// "center view" will be centered horizontally, and a little above centered vertically
centerView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
centerView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -25),
// left view is aligned to top of center view, and 20-pts to the left
leftView.topAnchor.constraint(equalTo: centerView.topAnchor),
leftView.trailingAnchor.constraint(equalTo: centerView.leadingAnchor, constant: -20),
// right view is aligned to top of center view, and 20-pts to the right
rightView.topAnchor.constraint(equalTo: centerView.topAnchor),
rightView.leadingAnchor.constraint(equalTo: centerView.trailingAnchor, constant: 20),
// bottom view is centerX aligned to center view, and 20-pts below
bottomView.topAnchor.constraint(equalTo: centerView.bottomAnchor, constant: 20),
bottomView.centerXAnchor.constraint(equalTo: centerView.centerXAnchor),
])
}
}
let viewController = AlignViewController()
PlaygroundPage.current.liveView = viewController
There are many ways to do things in autolayout. One way to solve this is to set the Leading and Trailing autolayout constraints for the playgroundimg view. That would make playgroundimg.frame.width adapt to the device screen width. And make sure that the constraints of the playgroundimg's superview are set.
In Xcode it would look like this for example:
You are using playgroundimg.frame.width in calculating the h and w variables, which I would suggest changing their names to x and y respectively since they denote coordinates and not a width or height:
x = self.playgroundimg.frame.width/3 - 48.89/2
y = self.playgroundimg.frame.height/4 - 17
And update this line too :
x = x + self.playgroundimg.frame.width/3 - 48.89 - 15

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

How to create a button with an icon on the right-hand side?

I am trying to put an icon next to a button label.
Problem: Interface Builder puts the button-image not to the right side. So there is still some space next to the (x).
Thanks for your help
You can do this in Interface Builder by setting the title and image insets:
In this case I've set the title right inset to 30 and the image left inset to 80. You can achieve the same in code by setting the UIButton's imageEdgeInset and titleEdgeInset properties. Knowing the size of the UIButton's subviews, you could probably calculate the edge insets using something like this:
CGSize labelWidth = myButton.titleLabel.frame.size.width;
CGSize imageWidth = myButton.imageView.frame.size.width;
myButton.titleEdgeInsets = (UIEdgeInsets){0.0, -imageWidth, 0.0, imageWidth};
myButton.imageEdgeInsets = (UIEdgeInsets){0.0, labelWidth, 0.0, -labelWidth};
In Xcode 8 you will find those attributes in the Size inspector:
select image(if (x) is image) or title (if (x) is simply key) on the window and set left , Right offsets according to your need
i have achieved this:
class CustomButton: UIButton {
open var rightIcon: UIImage? {
didSet {
rightIconImage = UIImageView(image: rightIcon?.withRenderingMode(.alwaysTemplate))
rightIconImage?.tintColor = .white
if let rightIconImage = rightIconImage {
addSubview(rightIconImage)
}
layoutSubviews()
}
}
open override func layoutSubviews() {
super.layoutSubviews()
setupRightIconIfNeeded()
}
func setupRightIconIfNeeded() {
if let rightIconImage = rightIconImage {
rightIconImage.translatesAutoresizingMaskIntoConstraints = false
titleLabel?.translatesAutoresizingMaskIntoConstraints = false
if let titleLabel = titleLabel, titleLabel.frame.width > 0 {
let paddingLeft = (frame.width - titleLabel.frame.width - spacing - iconSize) / 2
NSLayoutConstraint.deactivate(constraints)
NSLayoutConstraint.deactivate(titleLabel.constraints)
NSLayoutConstraint.activate([
//set height constraint for button
heightAnchor.constraint(equalToConstant: apolloSize.height),
//set constraint for title label
titleLabel.widthAnchor.constraint(equalToConstant: titleLabel.frame.width),
titleLabel.heightAnchor.constraint(equalToConstant: titleLabel.frame.height),
titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: paddingLeft),
//set constraint for right icon
rightIconImage.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: spacing),
rightIconImage.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
rightIconImage.widthAnchor.constraint(equalToConstant: iconSize),
rightIconImage.heightAnchor.constraint(equalToConstant: iconSize)
])
}
}
}
}

Resources