getting subview height to estimate another view height - ios

Hi i want to get the subviews height to estimate the detailUIView heightAnchor. In the detailUIView i have 4 labels that layedout using auto constraints(programmatically) and i want to get the views height so i can estimate the detailUIView.In the function estimateCardHeight i am iterating all the subviews and trying to get the height of each subview and adding them but i am getting 0.0. Whats am i missing here i couldn't figure it out.
class ComicDetailViewController: UIViewController {
var comicNumber = Int()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationItem.title = "Comic Number: \(comicNumber)"
}
let detailUIView = DetailComicUIView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// Do any additional setup after loading the view.
detailUIView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(detailUIView)
addConstraints()
print(estimateCardHeight())
}
public func configure(with viewModel: XKCDTableViewCellVM){
detailUIView.configure(with: viewModel)
comicNumber = viewModel.number
}
private func estimateCardHeight() -> CGFloat{
var allHeight : CGFloat = 0
for view in detailUIView.subviews{
allHeight += view.frame.origin.y
}
return allHeight
}
private func addConstraints(){
var constraints = [NSLayoutConstraint]()
constraints.append(detailUIView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 10))
constraints.append(detailUIView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 20))
constraints.append(detailUIView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -20))
constraints.append(detailUIView.heightAnchor.constraint(equalToConstant: self.view.frame.height/3))
NSLayoutConstraint.activate(constraints)
}
}

you are missing view.layoutIfNeeded()
addConstraints()
view.layoutIfNeeded()
print(estimateCardHeight())
as apple's saying
Use this method to force the view to update its layout immediately. When using Auto Layout, the layout engine updates the position of views as needed to satisfy changes in constraints.
Lays out the subviews immediately, if layout updates are pending.
And I guess:
the way to estimate height,
private func estimateCardHeight() -> CGFloat{
var allHeight : CGFloat = 0
for view in detailUIView.subviews{
allHeight += view.frame.size.height
// allHeight += view.frame.origin.y
}
return allHeight
}

An other way to achieve it.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(estimateCardHeight())
}
viewDidLayoutSubviews()
Called to notify the view controller that its view has just laid out its subviews.

Related

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.

Stretchy Layout not working with child view controller

I'm trying to follow the example described here for making a stretchy layout which includes a UIImageView and UIScrollView. https://github.com/TwoLivesLeft/StretchyLayout/tree/Step-6
The only difference is that I replace the UILabel used in the example with the view of a child UIViewController which itself contains a UICollectionView. This is how my layout looks - the blue items are the UICollectionViewCell.
This is my code:
import UIKit
import SnapKit
class HomeController: UIViewController, UIScrollViewDelegate {
private let scrollView = UIScrollView()
private let imageView = UIImageView()
private let contentContainer = UIView()
private let collectionViewController = CollectionViewController()
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func viewDidLoad() {
super.viewDidLoad()
scrollView.contentInsetAdjustmentBehavior = .never
scrollView.delegate = self
imageView.image = UIImage(named: "burger")
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
let imageContainer = UIView()
imageContainer.backgroundColor = .darkGray
contentContainer.backgroundColor = .clear
let textBacking = UIView()
textBacking.backgroundColor = #colorLiteral(red: 0.7450980544, green: 0.1235740449, blue: 0.2699040081, alpha: 1)
view.addSubview(scrollView)
scrollView.addSubview(imageContainer)
scrollView.addSubview(textBacking)
scrollView.addSubview(contentContainer)
scrollView.addSubview(imageView)
self.addChild(collectionViewController)
contentContainer.addSubview(collectionViewController.view)
collectionViewController.didMove(toParent: self)
scrollView.snp.makeConstraints {
make in
make.edges.equalTo(view)
}
imageContainer.snp.makeConstraints {
make in
make.top.equalTo(scrollView)
make.left.right.equalTo(view)
make.height.equalTo(imageContainer.snp.width).multipliedBy(0.7)
}
imageView.snp.makeConstraints {
make in
make.left.right.equalTo(imageContainer)
//** Note the priorities
make.top.equalTo(view).priority(.high)
//** We add a height constraint too
make.height.greaterThanOrEqualTo(imageContainer.snp.height).priority(.required)
//** And keep the bottom constraint
make.bottom.equalTo(imageContainer.snp.bottom)
}
contentContainer.snp.makeConstraints {
make in
make.top.equalTo(imageContainer.snp.bottom)
make.left.right.equalTo(view)
make.bottom.equalTo(scrollView)
}
textBacking.snp.makeConstraints {
make in
make.left.right.equalTo(view)
make.top.equalTo(contentContainer)
make.bottom.equalTo(view)
}
collectionViewController.view.snp.makeConstraints {
make in
make.left.right.equalTo(view)
make.top.equalTo(contentContainer)
make.bottom.equalTo(view)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.scrollIndicatorInsets = view.safeAreaInsets
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: view.safeAreaInsets.bottom, right: 0)
}
//MARK: - Scroll View Delegate
private var previousStatusBarHidden = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if previousStatusBarHidden != shouldHideStatusBar {
UIView.animate(withDuration: 0.2, animations: {
self.setNeedsStatusBarAppearanceUpdate()
})
previousStatusBarHidden = shouldHideStatusBar
}
}
//MARK: - Status Bar Appearance
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
override var prefersStatusBarHidden: Bool {
return shouldHideStatusBar
}
private var shouldHideStatusBar: Bool {
let frame = contentContainer.convert(contentContainer.bounds, to: nil)
return frame.minY < view.safeAreaInsets.top
}
}
Everything is the same as in this file: https://github.com/TwoLivesLeft/StretchyLayout/blob/Step-6/StretchyLayouts/StretchyViewController.swift with the exception of the innerText being replaced by my CollectionViewController.
As you can see, the UICollectionView is displayed properly - however I am unable to scroll up or down anymore. I'm not sure where my mistake is.
It looks like you are constraining the size of your collection view to fit within the bounds of the parent view containing the collection view's container view and the image view. As a result, the container scrollView has no contentSize to scroll over, and that's why you can't scroll. You need to ensure your collection view's content size is reflected in the parent scroll view's content size.
In the example you gave, this behavior was achieved by the length of the label requiring a height greater than the height between the image view and the rest of the view. In your case, the collection view container needs to behave as if it's larger than that area.
Edit: More precisely you need to pass the collectionView.contentSize up to your scrollView.contentSize. A scrollview's contentSize is settable, so you just need to increase the scrollView.contentSize by the collectionView.contentSize - collectionView.height (since your scrollView's current contentSize currently includes the collectionView's height). I'm not sure how you are adding your child view controller, but at the point you do that, I would increment your scrollView's contentSize accordingly. If your collectionView's size changes after that, though, you'll also need to ensure you delegate that change up to your scrollView. This could be accomplished by having a protocol such as:
protocol InnerCollectionViewHeightUpdated {
func collectionViewContentHeightChanged(newSize: CGSize)
}
and then making the controller containing the scrollView implement this protocol and update the scrollView contentSize accordingly. From your collectionView child controller, you would have a delegate property for this protocol (set this when creating the child view controller, setting the delegate as self, the controller containing the child VC and also the scrollView). Then whenever the collectionView height changes (if you add cells, for example) you can do delegate.collectionViewContentHeightChanged(... to ensure your scroll behavior will continue to function.

Unable to set the width of scroll View iOS Swift

I am unable to set the width of the scrollView to the width of the screen. Could someone please guide where I am going wrong ?
Here is how I have enabled a scroll view
// SCROLL VIEW
var scrollView = UIScrollView()
var contentView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
screenHeight = screenSize.height
screenWidth = screenSize.width
// SCROLL VIEW
scrollView = UIScrollView(frame: view.bounds)
// Here I am adding all my labels and textview fields to content view
contentView.addSubview(problemDescriptionStackView)
....
// Adding content view to the scrollView
scrollView.addSubview(contentView)
// Pinning the contentView to the scroll view
pinView(contentView, to: scrollView)
self.view.addSubview(scrollView)
// Here I am Setting the constraints for all the items in the contentView
....
}
// The below functions are used to pin one view to another view
public func pinView(_ view: UIView, to scrollView: UIScrollView) {
view.translatesAutoresizingMaskIntoConstraints = false
view.pin(to: scrollView)
}
public func pin(to view: UIView) {
NSLayoutConstraint.activate([
leadingAnchor.constraint(equalTo: view.leadingAnchor),
trailingAnchor.constraint(equalTo: view.trailingAnchor),
topAnchor.constraint(equalTo: view.topAnchor),
bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
Use below methods for get exact size of screen accordingly iPhone.
Hope using this method, your problem solved. If you have doubt Please comment here.
//MARK: - Main Screen Width
func getScreenWidth() -> CGFloat{
return UIScreen.main.bounds.size.width
}
//MARK: - Main Screen Height
func getScreenHeight() -> CGFloat{
return UIScreen.main.bounds.height
}
viewDidLoad load your ui from storyboard or nib. And size depend on what you select in Interface Builder.
You can initialize ui in viewWillAppear and add a variable isInitialized .
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if !isInitialized {
isInitialized = true
// do your stuff
}
}
Scroll.contentSize = CGSize(width: self.view.frame.size.width, height: self.view.frame.size.height)
make one subview horizontally center (using autolayout constraint) to a scrollview in interface builder or by programmatically
Ok so there are a couple of ways you can fix this:
1.- in the storyboard go to your view and you're going to hold the left click in your scroll view and release it over the main view, then you'll click equal width
2.- in the view did load put
self.scrollViewName.frame.size.width = self.view.frame.width
You can set the contentInset on the scrollView, and it will adjust the content size of the scrolling area within the scrollView, here is a quick example
scrollView.contentInset UIEdgeInsets(top: 0, left: 0, bottom: 0, right: padding)
That should do the trick :)
Try following code, may be you are not able to change frame because of autoresizing, So try to write following code in viewDidAppear method
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Set the frame of scrollview
self.scrollViewName.frame = CGRectMake(self.scrollViewName.frame.origin.x,
self.scrollViewName.frame.origin.y, self.view.frame.size.width, self.view.frame.size.height);
}
Pin your scrollView to view instead of setting its frame.
Add a constraint that sets the width of the contentView equal to the width of the scrollView:
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
As you are setting constraints from viewDidLoad, but view is not properly layout in this method. You should set constraint when lay out of views is done. So you should set constraints in :
override func viewDidLayoutSubviews() {
// set constraints here
}
And you can use a variable to check if constraints are already added.

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
}

Resources