sequence of auto layout methods get called in rendering cycle - ios

I was reading about auto layout rendering pipelines i mean how auto layout work under the hood. There are some methods which get called at different stages of autoLayout rendering like
layoutIfNeeded()
layoutSubviews()
updateConstraints()
updateConstraintsIfNeeded()
but i don't know which method is called when and what is the significance of that method and if i want to use auto layout then in which order i can use that methods and how can i control the autoLayout rendering pipeline

Usually you don't need to care about the autolayout method chain. You just need to create the constraints for the views to define their sizes and positions. You can add/remove, activate/deactivate constraints anytime in lifecycle of the view, but you want to always have a set of satisfiable (non-conflicting), yet complete set of constraints.
Take an example. You can tell the autolayout that button A should be 50 points wide, 20 points high, with its left top corner positioned at point (0,0) in the viewController's view. Now, this is non-conflicting, yet complete set of constraints for the button A. But lets say you want to expand that button, when the user taps it. So in the tap handler you will add one new constraint saying that the button should be 100 points wide - now you have unsatisfiable constraints - there is a constraint say it should be 50 points wide, and another one saying it shoul be 100 points wide. Therefore, to prevent conflict, before activating the new constraint, you have to deactivate the old one. Incomplete constraints is an opposite case, lets say you deactivate the old width constraint, but never activate the new one. Then autolayout can calculate position (because there are constraints defining it), and height, but not width, which usually ends in undefined behavior (now in case of a UIButton that's not true, because it has intrinsic size, which implicitly defines its width and height, but I hope you get the point).
So when you create those constraints is up to you (in my example you were manipulating them when the user tapped the button). Usually you start in initializer in case of a UIView subclass or in loadView in UIViewController subclass and there you can define and activate the default set of constraints. And then you can use handlers to react to user activity. My recommendation is prepare all the constraints in loadView, keep them in properties, and activate/deactivate them when necessary.
But there are of course some limitation as when and how not to create new constraints - for a more detailed discussion of those cases I really recommend looking at Advanced Autolayout Toolbox by objc.io.
EDIT
See following example of a simple custom SongView that uses autolayout for layout and supports also some dynamic changes in constraints by activating/deactivating them. You can just simply copy paste the whole code into a playground and test it out there, or include it in a project.
Notice there that I don't call any of the autolayout lifecycle methods, except of setNeedsLayout and layoutIfNeeded. setNeedsLayout sets a flag telling the autolayout that constraints have been changed, and layoutIfNeeded then tells it to recalculate frames. Normally, that would happen automatically, but to animate the constraints changes we need to tell it explicitly - see the setExpanded method in SongView. For more detailed explanation of using autolayout in animations, see my different answer.
import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
let songView = SongView()
let button = UIButton()
override func loadView() {
super.loadView()
view.backgroundColor = .white
self.view.addSubview(button)
self.view.addSubview(songView)
button.setTitle("Expand/Collapse", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(expandCollapse), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
songView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// button has intrinsic size, no need to define constraints for size, position is enough
button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -50),
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
// songView has defined its height (see SongView class), but not width, therefore we need more constraints
songView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
songView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
songView.rightAnchor.constraint(equalTo: self.view.rightAnchor),
])
}
#objc func expandCollapse() {
if songView.isExpanded {
songView.setExpanded(to: false, animated: true)
} else {
songView.setExpanded(to: true, animated: true)
}
}
}
class SongView: UIView {
private let numberLabel: UILabel = UILabel()
private let nameLabel: UILabel = UILabel()
private var expandedConstraints: [NSLayoutConstraint] = []
private var collapsedConstraints: [NSLayoutConstraint] = []
// this can be triggered by some event
private(set) var isExpanded: Bool = false
func setExpanded(to expanded: Bool, animated: Bool) {
self.isExpanded = expanded
if animated {
if expanded {
// setup expanded state
NSLayoutConstraint.deactivate(collapsedConstraints)
NSLayoutConstraint.activate(expandedConstraints)
} else {
// setup collapsed
NSLayoutConstraint.deactivate(expandedConstraints)
NSLayoutConstraint.activate(collapsedConstraints)
}
self.setNeedsLayout()
UIView.animate(withDuration: 0.2, animations: {
self.layoutIfNeeded()
})
} else {
// non animated version (no need to explicitly call setNeedsLayout nor layoutIfNeeded)
if expanded {
// setup expanded state
NSLayoutConstraint.deactivate(collapsedConstraints)
NSLayoutConstraint.activate(expandedConstraints)
} else {
// setup collapsed
NSLayoutConstraint.deactivate(expandedConstraints)
NSLayoutConstraint.activate(collapsedConstraints)
}
}
}
var data: (String, String)? {
didSet {
numberLabel.text = data?.0
nameLabel.text = data?.1
}
}
init() {
super.init(frame: CGRect.zero)
setupInitialHierarchy()
setupInitialAttributes()
setupInitialLayout()
}
fileprivate func setupInitialHierarchy() {
self.addSubview(numberLabel)
self.addSubview(nameLabel)
}
fileprivate func setupInitialAttributes() {
numberLabel.font = UIFont.boldSystemFont(ofSize: UIFont.preferredFont(forTextStyle: UIFontTextStyle.body).pointSize)
numberLabel.textColor = UIColor.darkGray
numberLabel.text = "0"
numberLabel.textAlignment = .right
nameLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
nameLabel.text = "NONE"
nameLabel.textAlignment = .left
self.backgroundColor = UIColor.lightGray
}
fileprivate func setupInitialLayout() {
self.translatesAutoresizingMaskIntoConstraints = false
numberLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.translatesAutoresizingMaskIntoConstraints = false
// just randomly selected different layouts for collapsed and expanded states
expandedConstraints = [
numberLabel.widthAnchor.constraint(equalToConstant: 35),
self.heightAnchor.constraint(equalToConstant: 80),
]
collapsedConstraints = [
numberLabel.widthAnchor.constraint(equalToConstant: 50),
self.heightAnchor.constraint(equalToConstant: 40),
]
// activating collapsed as default layout
NSLayoutConstraint.activate(collapsedConstraints)
NSLayoutConstraint.activate([
numberLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 4),
numberLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -4),
numberLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 4),
nameLabel.centerYAnchor.constraint(equalTo: numberLabel.centerYAnchor),
nameLabel.leftAnchor.constraint(equalTo: numberLabel.rightAnchor, constant: 8),
nameLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -4)
])
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
PlaygroundPage.current.liveView = ViewController()

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.

Unable to activate constraint with anchors error with UIimageView

Posting a question for the first time here.
So I have been trying to make an animation of an UIimageView. I did that so far. So the image moves from the middle of the screen to the top. I want to be able to make that animation with constraints. But while trying to add some constraints, I receive this error "Unable to activate constraint with anchors error".
here is the code which I try to add some constraints to banditLogo imageview.
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(banditLogo)
view.translatesAutoresizingMaskIntoConstraints = false // autolayout activation
chooseLabel.alpha = 0
signInButtonOutlet.alpha = 0
self.banditLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 304).isActive = true
self.banditLogo.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 94).isActive = true
self.banditLogo.widthAnchor.constraint(equalToConstant: 224).isActive = true
self.banditLogo.heightAnchor.constraint(equalToConstant: 289).isActive = true
}
and here is the func that makes the animation.
this func is being called in viewDidAppear and animatedImage variable of the function is referred to banditLogo UIimageView.
so when the view screen loads up, the image moves to top of the view.
func logoAnimate(animatedImage: UIImageView!, animatedLabel: UILabel!) {
UIView.animate(withDuration: 1.5, delay: 1, options: [.allowAnimatedContent]) {
animatedImage.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 5).isActive = true
animatedImage.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor, constant: 94).isActive = true
} completion: { (true) in
UIView.animate(withDuration: 0.25) {
animatedLabel.alpha = 1
}
}
}
You may find it easier to create a class-level property to hold the image view's top constraint, then change that constraint's .constant value when you want to move it.
Here's a quick example - tapping anywhere on the view will animate the image view up or down:
class AnimLogoViewController: UIViewController {
let banditLogo = UIImageView()
// we'll change this constraint's .constant to change the image view's position
var logoTopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
if let img = UIImage(systemName: "person.fill") {
banditLogo.image = img
}
view.addSubview(banditLogo)
// I assume this was a typo... you want to set it on the image view, not the controller's view
//view.translatesAutoresizingMaskIntoConstraints = false // autolayout activation
banditLogo.translatesAutoresizingMaskIntoConstraints = false // autolayout activation
// create the image view's top constraint
logoTopConstraint = banditLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 304)
// activate it
logoTopConstraint.isActive = true
// non-changing constraints
self.banditLogo.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 94).isActive = true
self.banditLogo.widthAnchor.constraint(equalToConstant: 224).isActive = true
self.banditLogo.heightAnchor.constraint(equalToConstant: 289).isActive = true
// animate the logo when you tap the view
let t = UITapGestureRecognizer(target: self, action: #selector(self.didTap(_:)))
view.addGestureRecognizer(t)
}
#objc func didTap(_ g: UITapGestureRecognizer) -> Void {
// if the logo image view is at the top, animate it down
// else, animate it up
if logoTopConstraint.constant == 5.0 {
logoTopConstraint.constant = 304.0
} else {
logoTopConstraint.constant = 5.0
}
UIView.animate(withDuration: 1.5, animations: {
self.view.layoutIfNeeded()
})
}
}
I animate views that have constraints by changing constraints, not setting them. Leave the constraints that are static "as is" - that is, use isActive = true. But those you wish to change? Put them in two arrays and activate/deactivte them. Complete the animation like you are by using UIView.animate.
For instance, let's say you wish to move banditLogo from top 304 to top 5, which appears to me to be what you trying to do. Leave all other constraints as is - left (which your code doesn't seem to change), height, and width. Now, create two arrays:
var start = [NSLayoutConstraint]()
var finish = [NSLayoutConstraint]()
Add in the constraints that change. Note that I'm not setting them as active:
start.append(banditLogo.topAnchor.constraint(equalTo: safeAreaView.topAnchor, constant: 305))
finish.append(banditLogo.topAnchor.constraint(equalTo: safeAreaView.topAnchor, constant: 5))
Initialize things in viewDidLoad or any other view controller method as needed:
NSLayoutConstraint.activate(start)
Finally, when you wish to do the animation, deactivate/activate and tell the view to show the animation:
NSLayoutConstraint.deactivate(start)
NSLayoutConstraint.activate(finish)
UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() }
Last piece of critique, made with no intent of being offending.
Something in your code posted feels messy to me. Creating a function to move a single view should directly address the view IMHO, not pass the view into it. Maybe you are trying to move several views this way - in which case this is good code - but nothing in your question suggests it. It's okay to do the animation in a function - that way you can call it when needed. I do this all the time for something like this - sliding a tool overlay in and out. But if you are doing this to a single view, just address it directly. The code is more readable to other coders.
Also, my preference for the start is in viewDidLoad unless the VC is part of a navigation stack. But in that case, don't just use viewDidAppear, set things back to start in viewDidDisappear.
EDIT: looking at the comments, I assumed that yes you have already used translatesAutoresizingMaskIntoConstraints = false properly on every view needed.

Swift: How to fill a ScrollView from Interface Builder with UIViews programmatically

I am working on a project where I want the user to be able to select two methods of input for the same form. I came up with a scrollview that contains two custom UIViews (made programmatically). Here is the code for the responsible view controller:
import UIKit
class MainVC: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
var customView1: CustomView1 = CustomView1()
var customView2: customView2 = CustomView2()
var frame = CGRect.zero
func setupScrollView() {
pageControl.numberOfPages = 2
frame.origin.x = 0
frame.size = scrollView.frame.size
customView1 = customView1(frame: frame)
self.scrollView.addSubview(customView1)
frame.origin.x = scrollView.frame.size.width
frame.size = scrollView.frame.size
customView2 = CustomView2(frame: frame)
self.scrollView.addSubview(customView2)
self.scrollView.contentSize = CGSize(width: scrollView.frame.size.width * 2, height: scrollView.frame.size.height)
self.scrollView.delegate = self
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
pageControl.currentPage = Int(pageNumber)
}
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
scrollView.delegate = self
}
While it works, Xcode gives me an error message for auto layout:
Scrollable content size is ambiguous for "ScrollView"
Also a problem: content on the second UIView is not centered, even though it should be:
picture of the not centered content
import UIKit
class customView2: UIView {
lazy var datePicker: UIDatePicker = {
let datePicker = UIDatePicker()
datePicker.translatesAutoresizingMaskIntoConstraints = false
return datePicker
}()
//initWithFrame to init view from code
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
//initWithCode to init view from xib or storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
func setupView () {
self.backgroundColor = .systemYellow
datePicker.datePickerMode = .date
datePicker.addTarget(self, action: #selector(self.datePickerValueChanged(_:)), for: .valueChanged)
addSubview(datePicker)
setupLayout()
}
func setupLayout() {
let view = self
NSLayoutConstraint.activate([
datePicker.centerXAnchor.constraint(equalTo: view.centerXAnchor),
datePicker.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
datePicker.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
datePicker.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2)
])
}
#objc func datePickerValueChanged(_ sender: UIDatePicker) {
let dateFormatter: DateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd.MM.yyyy"
let selectedDate: String = dateFormatter.string(from: sender.date)
print("Selected value \(selectedDate)")
}
Any ideas on how to solve this? Thank you very much in advance. And please go easy on me, this is my first question on stackoverflow. I am also fairly new to programming in swift.
To make things easier on yourself,
add a horizontal UIStackView to the scroll view
set .distribution = .fillEqually
constrain all 4 sides to the scroll view's .contentLayoutGuide
constrain its height to the scroll view's .frameLayoutGuide
add your custom views to the stack view
constrain the width of the first custom view to the width of the scroll view's .frameLayoutGuide
Here is your code, modified with that approach:
class MainVC: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
var customView1: CustomView1 = CustomView1()
var customView2: CustomView2 = CustomView2()
func setupScrollView() {
pageControl.numberOfPages = 2
// let's put the two custom views in a horizontal stack view
let stack = UIStackView()
stack.axis = .horizontal
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(customView1)
stack.addArrangedSubview(customView2)
// add the stack view to the scroll view
scrollView.addSubview(stack)
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain stack view to all 4 sides of content layout guide
stack.topAnchor.constraint(equalTo: contentG.topAnchor),
stack.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
stack.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
// stack view Height equal to scroll view frame layout guide height
stack.heightAnchor.constraint(equalTo: frameG.heightAnchor),
// stack is set to fillEqually, so we only need to set
// width of first custom view equal to scroll view frame layout guide width
customView1.widthAnchor.constraint(equalTo: frameG.widthAnchor),
])
self.scrollView.delegate = self
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
pageControl.currentPage = Int(pageNumber)
}
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
scrollView.delegate = self
}
}
Edit
Couple additional notes...
UIScrollView layout ambiguity.
As I said in my initial comment, if we add a UIScrollView in Storyboard / Interface Builder, but do NOT give it any constrained content, IB will complain that it has Scrollable Content Size Ambiguity -- because it does. We haven't told IB what the content will be.
We can either ignore it, or select the scroll view and, at the bottom of the Size Inspector pane, change Ambiguity to Never Verify.
As a general rule, you should correct all auto-layout warnings / errors, but in specific cases such as this - where we know that it's setup how we want, and we'll be satisfying constraints at run-time - it doesn't hurt to leave it alone.
UIDatePicker not being centered horizontally.
It actually is centered. If you add this line:
datePicker.backgroundColor = .green
You'll see that the object frame itself is centered, but the UI elements inside the frame are left-aligned:
From quick research, it doesn't appear that can be changed.
Now, from Apple's docs, we see:
You should integrate date pickers in your layout using Auto Layout. Although date pickers can be resized, they should be used at their intrinsic content size.
Curiously, if we add a UIDatePicker in Storyboard, change its Preferred Style to Compact, and give it centerX and centerY constraints... Storyboard doesn't believe it has an intrinsic content size.
If we add it via code, giving it only X/Y position constraints, it will show up where we want it at its intrinsic content size. But... if we jump into Debug View Hierarchy, Xcode tells us its Position and size are ambiguous.
Now, what's even more fun...
Tap that control and watch the Debug console fill with 535 Lines of auto-layout errors / warnings!!!
Some quick investigation -- these are all internal auto-layout issues, and have nothing to do with our code or layout.
We see similar issues with the iOS built-in keyboard when it starts showing auto-complete options.
Those are safe to ignore.

UIButton image automatically resizes even with constraints

I have a minor issue, which I am probably not able to get my head around. I have a uibutton added programmatically. To that button I am adding a UIImage. I am just adding basic constraints, but for some reason it tends to shrink to a smaller size while running on screen.
Following is the code I have for generating the button:
var shareButton: UIButton!
func addShareButton() {
shareButton = UIButton()
shareButton.autoresizesSubviews = false
shareButton.clipsToBounds = true
shareButton.translatesAutoresizingMaskIntoConstraints = false
shareButton.adjustsImageWhenHighlighted = false
shareButton.setImage(UIImage(named: "wshare"), for: .normal)
shareButton.addTarget(self, action: #selector(shareButtonPressed), for: .touchDown)
shareButton.addTarget(self, action: #selector(shareButtonReleased), for: .touchUpInside)
view.addSubview(shareButton)
}
func addConstraints() {
NSLayoutConstraint.activate([
// Share Button
shareButton.widthAnchor.constraint(equalToConstant: 60),
shareButton.heightAnchor.constraint(equalToConstant: 60),
shareButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -15),
shareButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -25),
])
}
override func updateViewConstraints() {
addConstraints()
super.updateViewConstraints()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
addShareButton()
}
And what it generates is this:
The bigger box is supposed to be the actual size which is 60x60, but it magically shrinks to 32x32.
Can you help me point the issue. I know I am missing something quite silly, But I am not able get my head around it.
Personally, I like to keep my objects separate. While UIButton has a setImage(:UIImage) method, I prefer to create a UIButton then add my own UIImageView to it then add my own UIImage. So, in hierachy
-- UIButton
-- UIImageView
-- UIImage
With this, you can specify the specific bounds and any other variables associated to UIImageView. I find this methodology to be incredibly more concrete, especially if you have to alter the UIImageView.

Resources