Circular views with Autolayout (snapkit)? - ios

I am trying to make a circular view which has an adaptive size based on auto layout, currently i set the constraints, then i attempt to round the image in the viewwilllayoutsubviews method.
This is resulting in oddly shaped views that are not circular, how can i resolve this?
init:
profilePic = UIImageView(frame: CGRect.zero)
profilePic.clipsToBounds = true
profilePic.contentMode = .scaleAspectFill
constrains:
profilePic.snp.makeConstraints { (make) -> Void in
make.centerX.equalTo(self).multipliedBy(0.80)
make.centerY.equalTo(self).multipliedBy(0.40)
make.size.equalTo(self).multipliedBy(0.22)
}
subviews:
override func viewWillLayoutSubviews() {
self.navigationMenuView.profilePic.layer.cornerRadius = self.navigationMenuView.profilePic.frame.size.width / 2.0
self.navigationMenuView.profilePic.layer.borderWidth = 2
self.navigationMenuView.profilePic.layer.borderColor = UIColor.white.cgColor
}
result:

I guess you want this (sorry for the plain autolayout, but I don't use snapkit):
profilePic.heightAnchor.constraint(equalTo: profilePic.widthAnchor).isActive = true
profilePic.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.22).isActive = true
Instead of this:
make.size.equalTo(self).multipliedBy(0.22)

I had the same problem
This is my solution:
let profilePicHeight: CGFloat = 30.0
Add this line of code to your constrains:
profilePic.snp.makeConstraints { (make) -> Void in
make.height.width.equalTo(self.profilePicHeight)
...
}
then:
override func viewWillLayoutSubviews() {
self.navigationMenuView.profilePic.layer.cornerRadius = self.profilePicHeight / 2.0
...
}

My suggestion here is don't treat it like a circular view from the outside of it. Make the view itself conform to being a circle so that you can use it anywhere.
INSIDE the view give it constraints like...
widthAnchor.constraint(equalTo: heightAnchor).isActive = true
This will make it square (with undetermined size).
Then in the function layoutSubviews...
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.width * 0.5
}
This will make the square into a circle.

Related

UIView animation snaps into updated bounds before animation is done

Problem:
I am trying to create my own custom search field with a desired growing animation (if you click on it), and a shrinking animation when the user taps out.
The animation behaves weirdly since it moves out of the right screen bounds when shrinking, even though the text field/search bar's right anchor is not modified.
Like so:
Notice how the right side of the search bar briefly moves outside of the visible screen bounds during the animation.
Expected behavior:
The search bar should smoothly grow/shrink without moving the right edge position of the text field, i.e. have the right anchor stay pinned.
What you see in above gif is built using the following code (by subclassing a UITextField):
public class MySearchBar: UITextField {
private var preAnimationWidth: NSLayoutConstraint?
private var postAnimationWidth: NSLayoutConstraint?
public override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = Theme.GRAY800
self.borderStyle = .roundedRect
self.layer.masksToBounds = true
self.clipsToBounds = true
self.autocorrectionType = .no
self.font = FontFamily.ProximaNova.regular.font(size: 16)
self.textColor = .white
self.attributedPlaceholder = NSAttributedString(string: "Search", attributes: [.foregroundColor : Theme.GRAY400, .font: FontFamily.ProximaNova.regular.font(size: 16)])
// some further appearance configurations
}
public func setupGrowAnimation(initialWidth: NSLayoutConstraint, grownWidth: NSLayoutConstraint, height: CGFloat) {
preAnimationWidth = initialWidth
postAnimationWidth = grownWidth
self.layer.borderWidth = 0
self.layer.cornerRadius = height / 2
}
// growButton is called when the textfield becomes active, i.e. the user taps on it.
public func growButton() {
guard let preAnimationWidth = preAnimationWidth, let postAnimationWidth = postAnimationWidth else { return }
UIView.animate(withDuration: 0.2) {
preAnimationWidth.isActive = false
postAnimationWidth.isActive = true
self.layer.borderColor = Theme.GRAY600.cgColor
self.layer.borderWidth = 2
self.layer.cornerRadius = 8
self.layoutIfNeeded()
}
}
// shrinkButton is called whenever the textfield resigns its first responder state, i.e. the user clicks out of it.
public func shrinkButton() {
guard let preAnimationWidth = preAnimationWidth, let postAnimationWidth = postAnimationWidth else { return }
UIView.animate(withDuration: 0.2) {
postAnimationWidth.isActive = false
preAnimationWidth.isActive = true
self.layer.borderWidth = 0
self.layer.borderColor = .none
self.layer.cornerRadius = self.frame.height / 2
self.layoutIfNeeded()
}
}
}
And this is how the search bar is initialized in my viewDidLoad:
override func viewDidLoad() {
let containerView = UIView()
let searchBar = MySearchBar()
searchBar.addTarget(self, action: #selector(searchBarChangedEntry(_:)), for: .editingChanged)
searchBar.addTarget(self, action: #selector(searchBarEndedEditing(_:)), for: .editingDidEnd)
searchBar.translatesAutoresizingMaskIntoConstraints = false
let initialWidth = searchBar.widthAnchor.constraint(equalToConstant: 100)
let expandedWidth = searchBar.widthAnchor.constraint(equalTo: containerView.widthAnchor, constant: -32)
searchBar.setupGrowAnimation(initialWidth: initialWidth, grownWidth: expandedWidth, height: 44)
containerView.addSubview(searchBar)
stackView.insertArrangedSubview(containerView, at: 0)
NSLayoutConstraint.activate([
containerView.heightAnchor.constraint(equalToConstant: 44),
containerView.widthAnchor.constraint(equalTo: self.stackView.widthAnchor),
searchBar.heightAnchor.constraint(equalTo: containerView.heightAnchor),
initialWidth,
searchBar.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: -16)
])
self.stackView.setCustomSpacing(12, after: containerView)
}
The search bar is part of a container view which, in turn, is the first (top) arranged subview of a stack view covering the entire screen's safeAreaLayout rectangle
What I already tried:
I have to perform the animation using constraints, and I've tried to animate it without using the width anchor (e.g. by animating the leftAnchor's constant). Nothing worked so far.
Upon googling, I couldn't really find anything helpful that would help me find a solution to this problem, which is why I am trying my luck here.
I do have to admit that I am not proficient with animations of iOS at all - so please bear with me if this is a simple mistake to fix.
So, why does the search bar behave that way? And how can I fix this?
A little tough to say, because the code you posted is missing a lot of information (for example, you don't show the creation of the stackView, nor where its being added to the view hierarchy).
However, you might fix your issue with this simple change...
In both your growButton() and shrinkButton() funcs, change this line in the animation block:
self.layoutIfNeeded()
to this:
self.superview?.layoutIfNeeded()
Edit - a little explanation...
To animate constraint changes, we want to call .layoutIfNeeded() on the top-most view that will be affected.
When calling:
UIView.animate(withDuration: 0.5) {
self.someView.layoutIfNeeded()
}
we're telling auto-layout to calculate the changes and then generate and run an animation... but only for someView and its subviews.
If our action is going to affect someView.superview - or, for example, the constraint change is going to move/size a sibling of someView or a sibling of someView.superview, we haven't told auto-layout to include those views in its layout calculations.
I expect there are specific implementations / layout hierarchies where one would want to specifically exclude some views from the layout / animation... but...
Personally, I do this:
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
because the constraint I want to animate might be on a subview deep in the view hierarchy - and could have 4 or 5 or 6 etc superviews - all of which could be affected by the change.

Clip to bounds a particular view within a subview

Problem
I have a custom UIView that has an image and selection (border) subview. I want to be able to add this custom UIView as a subview of a larger blank view. Here's the catch, the larger blank view needs to clip all of the subviews to its bounds (clipToBounds). However, the user can select one of the custom UIViews within the large blank view, where the subview is then highlighted by a border.
The problem is that because the large blank view clips to bounds, the outline for the selected subview is cut off.
I want the image in the subview to clip to the bounds of the large blank view, but still be able to see the full selection outline of the subview (which is cut off due to the large blank view's corner radius.
I am using UIKit and Swift
👎 What I Currently Have:
👍 What I Want:
The image part of the subview clips to the bounds (corner radius) of the large blank view, but the outline selection view in the subview should not.
Thanks in advance for all your help!
I think what you are looking for is not technically possible as defined by the docs
From the docs:
clipsToBounds
Setting this value to true causes subviews to be clipped to the bounds of the receiver. If set to false, subviews whose frames extend beyond the visible bounds of the receiver are not clipped. The default value is false.
So the subviews do not have control of whether they get clipped or not, it's the container view that decides.
So I believe Matic's answer is right in that the structure he proposes gives you the most flexibility.
With that being said, here are a couple of work arounds I can think of:
First, set up to recreated your scenario
Custom UIView
// Simple custom UIView with image view and selection UIView
fileprivate class CustomBorderView: UIView
{
private var isSelected = false
{
willSet
{
toggleBorder(newValue)
}
}
var imageView = UIImageView()
var selectionView = UIView()
init()
{
super.init(frame: CGRect.zero)
configureImageView()
configureSelectionView()
}
required init?(coder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews()
{
super.layoutSubviews()
}
private func configureImageView()
{
imageView.image = UIImage(named: "image-test")
imageView.contentMode = .scaleAspectFill
addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
imageView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
private func configureSelectionView()
{
selectionView.backgroundColor = .clear
selectionView.layer.borderWidth = 3
selectionView.layer.borderColor = UIColor.clear.cgColor
addSubview(selectionView)
selectionView.translatesAutoresizingMaskIntoConstraints = false
selectionView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
selectionView.topAnchor.constraint(equalTo: topAnchor).isActive = true
selectionView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
selectionView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
configureTapGestureRecognizer()
}
private func configureTapGestureRecognizer()
{
let tapGesture = UITapGestureRecognizer(target: self,
action: #selector(didTapSelectionView))
selectionView.addGestureRecognizer(tapGesture)
}
#objc
private func didTapSelectionView()
{
isSelected = !isSelected
}
private func toggleBorder(_ on: Bool)
{
if on
{
selectionView.layer.borderColor = UIColor(red: 28.0/255.0,
green: 244.0/255.0,
blue: 162.0/255.0,
alpha: 1.0).cgColor
return
}
selectionView.layer.borderColor = UIColor.clear.cgColor
}
}
Then in the view controller
class ClippingTestViewController: UIViewController
{
private let mainContainerView = UIView()
private let customView = CustomBorderView()
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = .white
title = "Clipping view"
configureMainContainerView()
configureCustomBorderView()
mainContainerView.layer.cornerRadius = 50
mainContainerView.clipsToBounds = true
}
private func configureMainContainerView()
{
mainContainerView.backgroundColor = .white
view.addSubview(mainContainerView)
mainContainerView.translatesAutoresizingMaskIntoConstraints = false
mainContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor,
constant: 20).isActive = true
mainContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
constant: 20).isActive = true
mainContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor,
constant: -20).isActive = true
mainContainerView.heightAnchor.constraint(equalToConstant: 300).isActive = true
view.layoutIfNeeded()
}
private func configureCustomBorderView()
{
mainContainerView.addSubview(customView)
customView.translatesAutoresizingMaskIntoConstraints = false
customView.leadingAnchor.constraint(equalTo: mainContainerView.leadingAnchor).isActive = true
customView.topAnchor.constraint(equalTo: mainContainerView.safeAreaLayoutGuide.topAnchor).isActive = true
customView.trailingAnchor.constraint(equalTo: mainContainerView.trailingAnchor).isActive = true
customView.bottomAnchor.constraint(equalTo: mainContainerView.bottomAnchor).isActive = true
view.layoutIfNeeded()
}
}
This gives me your current experience
Work Around 1. - Shrink subviews on selection
When the view is not selected, everything looks fine. When the view is selected, you could reduce the width and height of the custom subview with some animation while adding the border.
Work Around 2. - Manually clip desired subviews
You go through each subview in your container view and:
Apply the clipping to any subview you desire
Apply the corner radius to the views you clip
Leaving the container view unclipped and without a corner radius
To do that, I created a custom UIView subclass for the container view
class ClippingSubView: UIView
{
override var clipsToBounds: Bool
{
didSet
{
if clipsToBounds
{
clipsToBounds = false
clipImageViews(in: self)
layer.cornerRadius = 0
}
}
}
// Recursively go through all subviews
private func clipImageViews(in view: UIView)
{
for subview in view.subviews
{
// I am only checking image view, you could check which you want
if subview is UIImageView
{
print(layer.cornerRadius)
subview.layer.cornerRadius = layer.cornerRadius
subview.clipsToBounds = true
}
clipImageViews(in: subview)
}
}
}
Then make sure to adjust the following lines where you create your views:
let mainContainerView = ClippingSubView()
// Do this only after you have added all the subviews for this to work
mainContainerView.layer.cornerRadius = 50
mainContainerView.clipsToBounds = true
This gives me your desired output
This is a pretty common problem which may have multiple solutions. In the end though I always find it best to simply go one level higher:
ContainerView (Does not clip)
ContentView (Clips)
HighlightingView (Does not clip)
You would put all your current views on ContentView. Then introduce another view which represents your selection and put it on the same level as your ContentView.
In the end this will give you most flexibility. It can still get a bit more complicated when you add things like shadows. But again "more views" is usually the end solution.
You'll likely run into a lot of problems trying to get a subview's border to display outside its superView's clipping bounds.
One approach is to add an "Outline View" as a sibling of the "Clipping View":
When you select a clippingView's subview - and drag it around - set the frame of the outlineView to match the frame of that subview.
You'll want to set .isUserInteractionEnabled = false on the outlineView so it doesn't interfere with touches on the subviews.

Retrieve the right position of a subView with auto-layout

I want to place programmatically a view at the center of all the the subviews created in a storyboard.
In the storyboard, I have a view, and inside a Vertical StackView, which has constraint to fill the full screen, distribution "Equal spacing".
Inside of the Vertical Stack View, I have 3 horizontal stack views, with constraint height = 100, and trailing and leading space : 0 from superview. The distribution is "equal spacing" too.
In each horizontal stack view, I have two views, with constraint width and height = 100, that views are red.
So far, so good, I have the interface I wanted,
Now I want to retrieve the center of each red view, to place another view at that position (in fact, it'll be a pawn over a checkboard...)
So I wrote that:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var verticalStackView:UIStackView?
override func viewDidLoad() {
super.viewDidLoad()
print ("viewDidLoad")
printPos()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print ("viewWillAppear")
printPos()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print ("viewWillLayoutSubviews")
printPos()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print ("viewDidLayoutSubviews")
printPos()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print ("viewDidAppear")
printPos()
addViews()
}
func printPos() {
guard let verticalSV = verticalStackView else { return }
for horizontalSV in verticalSV.subviews {
for view in horizontalSV.subviews {
let center = view.convert(view.center, to:nil)
print(" - \(center)")
}
}
}
func addViews() {
guard let verticalSV = verticalStackView else { return }
for horizontalSV in verticalSV.subviews {
for redView in horizontalSV.subviews {
let redCenter = redView.convert(redView.center, to:self.view)
let newView = UIView(frame: CGRect(x:0, y:0, width:50, height:50))
//newView.center = redCenter
newView.center.x = 35 + redCenter.x / 2
newView.center.y = redCenter.y
newView.backgroundColor = .black
self.view.addSubview(newView)
}
}
}
}
With that, I can see that in ViewDidLoad and ViewWillAppear, the metrics are those of the storyboard. The positions changed then in viewWillLayoutSubviews, in viewDidLayoutSubviews and again in viewDidAppear.
After viewDidAppear (so after all the views are in place), I have to divide x coordinate by 2 and adding something like 35 (see code) to have the new black view correctly centered in the red view. I don't understand why I can't simply use the center of the red view... And why does it works for y position ?
I found your issue, replace
let redCenter = redView.convert(redView.center, to:self.view)
with
let redCenter = horizontalSV.convert(redView.center, to: self.view)
When you convert, you have to convert from the view original coordinates, here it was the horizontalSv
So you want something like this:
You should do what Beninho85 and phamot suggest and use constraints to center the pawn over its starting square. Note that the pawn does not have to be a subview of the square to constrain them together, and you can add the pawn view and its constraints in code instead of in the storyboard:
#IBOutlet private var squareViews: [UIView]!
private let pawnView = UIView()
private var pawnSquareView: UIView?
private var pawnXConstraint: NSLayoutConstraint?
private var pawnYConstraint: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
let imageView = UIImageView(image: #imageLiteral(resourceName: "Pin"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
pawnView.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: pawnView.centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: pawnView.centerYAnchor),
imageView.widthAnchor.constraint(equalTo: pawnView.widthAnchor, multiplier: 0.8),
imageView.heightAnchor.constraint(equalTo: pawnView.heightAnchor, multiplier: 0.8)])
pawnView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pawnView)
let squareView = squareViews[0]
NSLayoutConstraint.activate([
pawnView.widthAnchor.constraint(equalTo: squareView.widthAnchor),
pawnView.heightAnchor.constraint(equalTo: squareView.heightAnchor)])
constraintPawn(toCenterOf: squareView)
let dragger = UIPanGestureRecognizer(target: self, action: #selector(draggerDidFire(_:)))
dragger.maximumNumberOfTouches = 1
pawnView.addGestureRecognizer(dragger)
}
Here are the helper functions for setting up the x and y constraints:
private func constraintPawn(toCenterOf squareView: UIView) {
pawnSquareView = squareView
self.replaceConstraintsWith(
xConstraint: pawnView.centerXAnchor.constraint(equalTo: squareView.centerXAnchor),
yConstraint: pawnView.centerYAnchor.constraint(equalTo: squareView.centerYAnchor))
}
private func replaceConstraintsWith(xConstraint: NSLayoutConstraint, yConstraint: NSLayoutConstraint) {
pawnXConstraint?.isActive = false
pawnYConstraint?.isActive = false
pawnXConstraint = xConstraint
pawnYConstraint = yConstraint
NSLayoutConstraint.activate([pawnXConstraint!, pawnYConstraint!])
}
When the pan gesture starts, deactivate the existing x and y center constraints and create new x and y center constraints to track the drag:
#objc private func draggerDidFire(_ dragger: UIPanGestureRecognizer) {
switch dragger.state {
case .began: draggerDidBegin(dragger)
case .cancelled: draggerDidCancel(dragger)
case .ended: draggerDidEnd(dragger)
case .changed: draggerDidChange(dragger)
default: break
}
}
private func draggerDidBegin(_ dragger: UIPanGestureRecognizer) {
let point = pawnView.center
dragger.setTranslation(point, in: pawnView.superview)
replaceConstraintsWith(
xConstraint: pawnView.centerXAnchor.constraint(equalTo: pawnView.superview!.leftAnchor, constant: point.x),
yConstraint: pawnView.centerYAnchor.constraint(equalTo: pawnView.superview!.topAnchor, constant: point.y))
}
Note that I set the translation of the dragger so that it is exactly equal to the desired center of the pawn view.
If the drag is cancelled, restore the constraints to the starting square's center:
private func draggerDidCancel(_ dragger: UIPanGestureRecognizer) {
UIView.animate(withDuration: 0.2) {
self.constraintPawn(toCenterOf: self.pawnSquareView!)
self.view.layoutIfNeeded()
}
}
If the drag ends normally, set constraints to the nearest square's center:
private func draggerDidEnd(_ dragger: UIPanGestureRecognizer) {
let squareView = nearestSquareView(toRootViewPoint: dragger.translation(in: view))
UIView.animate(withDuration: 0.2) {
self.constraintPawn(toCenterOf: squareView)
self.view.layoutIfNeeded()
}
}
private func nearestSquareView(toRootViewPoint point: CGPoint) -> UIView {
func distance(of candidate: UIView) -> CGFloat {
let center = candidate.superview!.convert(candidate.center, to: self.view)
return hypot(point.x - center.x, point.y - center.y)
}
return squareViews.min(by: { distance(of: $0) < distance(of: $1)})!
}
When the drag changes (meaning the touch moved), update the constants of the existing constraint to match the translation of the gesture:
private func draggerDidChange(_ dragger: UIPanGestureRecognizer) {
let point = dragger.translation(in: pawnView.superview!)
pawnXConstraint?.constant = point.x
pawnYConstraint?.constant = point.y
}
You lose your time with frames. Because there is AutoLayout, frames are moved and with frames you can add your views but it's too late in the ViewController lifecycle. Much easier and effective to use constraints/anchors, just take care to have views in the same hierarchy:
let newView = UIView()
self.view.addSubview(newView)
newView.translatesAutoresizingMaskIntoConstraints = false
newView.centerXAnchor.constraint(equalTo: self.redView.centerXAnchor).isActive = true
newView.centerYAnchor.constraint(equalTo: self.redView.centerYAnchor).isActive = true
newView.widthAnchor.constraint(equalToConstant: 50.0).isActive = true
newView.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
But to answer the initial problem maybe it was due to the fact that there is the stackview between so maybe coordinates are relative to the stackview instead of the container or something like that.
You need not divide the height and width of the coordinates to get the center of the view. You can use align option by selecting multiple views and add horizontal centers and vertical centers constraints.

Update SnapKit Constraint offset

I am using SnapKit and can't find a clean way to update the offset of a constraint. It's a simple loading bar view whose inner view (expiredTime) has to fill the parent (left to right) according to percentage.
I create the constraint in the awakeFromNib of a custom UIView
self.expiredTime.snp.makeConstraints { (make) in
make.left.equalTo(self)
make.top.equalTo(self)
make.bottom.equalTo(self)
self.constraintWidth = make.width.equalTo(self).constraint
}
setNeedsUpdateConstraints()
then whenever the time is updated i call setNeedsUpdateConstraints() which will trigger the default updateConstraints() which has been overloaded as by apple hint:
Does not work: (the expiredTime view always fit with the parent view)
override func updateConstraints() {
let offset = self.frame.width * CGFloat(percentage)
self.expiredTime.snp.updateConstraints { (make) in
make.width.equalTo(self).offset(offset).constraint
}
super.updateConstraints()
}
This also does not work:
override func updateConstraints() {
let offset = self.frame.width * CGFloat(percentage)
self.constraintWidth?.update(offset: offset)
super.updateConstraints()
}
Rebuilding all the constraint works but i would like to avoid it
override func updateConstraints() {
self.expiredTime.snp.remakeConstraints() { (make) in
make.left.equalTo(self)
make.top.equalTo(self)
make.bottom.equalTo(self)
self.constraintWidth = make.width.equalTo(self).multipliedBy(self.percentage).constraint
}
super.updateConstraints()
}
Your first solution does not work, because you already set the width of your expiredTime view to the full width before adding the offset. To make it work you have to set the width to 0 and then add the offset. But the offset is not really needed here, you can simply set the width to the calculated width:
override func updateConstraints() {
let width = self.frame.width * CGFloat(percentage)
self.expiredTime.snp.updateConstraints { (make) in
make.width.equalTo(width)
}
super.updateConstraints()
}
Or if you keep a reference to the constraint you don't have to override updateConstraints() at all. You can simply call the constraint's update method (without calling setNeedsUpdateConstraints() afterwards)
constraintWidth?.update(offset: CGFloat(percentage) * view.bounds.width)
Just remember to set the width to 0 when you initialize constraintWidth:
self.expiredTime.snp.makeConstraints { (make) in
make.left.top.bottom.equalTo(self)
self.constraintWidth = make.width.equalTo(0).constraint
}

UIButton, Autolayout and getting the height of the button

I'm trying to get the real height of my custom UIButton. But I always get a value that is way smaller than I expected. I get something around 30 but expected something about 45.
override func awakeFromNib() {
print(self.frame.height)
self.layer.cornerRadius = self.layout.frame.height / 2.0
self.clipsToBounds = true
self.titleLabel?.adjustsFontSizeToFitWidth = true
}
This is my code, but at runtime somehow autolayout changes the size, which is perfect, but I can not set the right cornerRadius (always too small). So maybe I need the multiplier or something like that. I already testet with adding the contentEdgeInsets of top and bottom, but it made no difference.
Thanks
Tobias
The view hasn't been laid out in awakeFromNib(). Set your corner radius when layout has guaranteed to have happened.
For a view:
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = frame.height / 2.0
}
For a UIViewController:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
view.layer.cornerRadius = view.frame.height / 2.0
}
Try calling self.layoutIfNeeded() before you print the size

Resources