Retrieve the right position of a subView with auto-layout - ios

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.

Related

getting subview height to estimate another view height

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.

Unsure how to mimic desired UI functionality

So I found the following UI pattern online and I have been attempting to implement it in Xcode. However, I have been unsuccessful. I am unsure as to whether to create the optimal approach would be to
create three different UIViewControllers (in which case I am not sure as to how to get them to animate in and out of view/how to get them to overlap one another)
or to use a UITableView with custom overlapping cells. However, I am not sure whether this approach will allow me to animate properly upon pressing each cell. For this second approach, I saw this post, but this solution does not allow for touch interaction in the overlapping areas, something which I need.
I looked for libraries online that would allow for functionality such as this, but I was unsuccessful in finding any. The animation I am trying to achieve can be found here.
I would use a StackView to hold all the views of your ViewControllers.
Then you can set the stack view's spacing property to a negative value to make the views overlap. If you wish show the complete view of one of the view controllers on tap, you add a tap gesture recognizer to that view that changes the stackView's spacing for just that view to 0.
A really simple example:
class PlaygroundViewController: UIViewController {
let firstViewController = UIViewController()
let secondViewController = UIViewController()
let thirdViewController = UIViewController()
lazy var viewControllersToAdd = [firstViewController, secondViewController, thirdViewController]
let heightOfView: CGFloat = 300
let viewOverlap: CGFloat = 200
let stackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
firstViewController.view.backgroundColor = .red
secondViewController.view.backgroundColor = .blue
thirdViewController.view.backgroundColor = .green
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = -viewOverlap
viewControllersToAdd.forEach { (controller: UIViewController) in
if let childView = controller.view {
stackView.addArrangedSubview(childView)
NSLayoutConstraint.activate([
childView.heightAnchor.constraint(equalToConstant: heightOfView),
childView.widthAnchor.constraint(equalTo: stackView.widthAnchor)
])
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapChildView(sender:)))
childView.addGestureRecognizer(gestureRecognizer)
childView.isUserInteractionEnabled = true
}
addChild(controller)
controller.didMove(toParent: self)
}
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.rightAnchor.constraint(equalTo: view.rightAnchor),
stackView.leftAnchor.constraint(equalTo: view.leftAnchor),
stackView.topAnchor.constraint(equalTo: view.topAnchor),
])
}
#objc func didTapChildView(sender: UITapGestureRecognizer) {
if let targetView = sender.view {
UIView.animate(withDuration: 0.3, animations: {
let currentSpacing = self.stackView.customSpacing(after: targetView)
if currentSpacing == 0 {
// targetView is already expanded, collapse it
self.stackView.setCustomSpacing(-self.viewOverlap, after: targetView)
} else {
// expand view
self.stackView.setCustomSpacing(0, after: targetView)
}
})
}
}
}

Circular views with Autolayout (snapkit)?

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.

How to remove a view and update constraints?

I have a view in which i am creating another view programmatically on add button through xib. I am able to create multiple views on tapping add more button and remove is also working if I remove last view but the problem occurs with middle views due to missing constraints view not updating correctly bellow are images how it is looking
At start view Look like this
After Adding more view
After removing middle view
Delete button code
#IBAction func deletebnt(_ sender: UIButton) {
let view = self.superview
let index = view?.subviews.index(of:self)!
delegate.txtcheck(text: countstr)
self.view.removeFromSuperview()
}
Add button Code
#IBAction func addMoreBnt(_ sender: UIButton) {
for constraint in addSuperview.constraints {
if constraint.firstAttribute == NSLayoutAttribute.height
{
constraint.constant += 45
space = constraint.constant
}
}
let newView : AvalabileTimeView = AvalabileTimeView()
newView.frame = CGRect(x: self.addsubView.frame.origin.x, y: 70, width: addsubView.frame.size.width, height:addsubView.frame.size.height)
newView.delegate = self as AvalabileTimeDelegate
addSuperview.addSubview(newView)
let index = addSuperview.subviews.index(of: newView)!
newView.translatesAutoresizingMaskIntoConstraints = false
let heightConstraint = newView.widthAnchor.constraint(equalToConstant:addsubView.frame.size.width )
let widthConstaint = newView.heightAnchor.constraint(equalToConstant:31 )
let topConstraint = newView.topAnchor.constraint(equalTo: addSuperview.topAnchor, constant: space - 31) NSLayoutConstraint.activate([heightConstraint,topConstraint,widthConstaint])
}
delegate to change height of superview
func txtcheck(text: String!) {
print(text)
for constraint in addSuperview.constraints {
if constraint.firstAttribute == NSLayoutAttribute.height
{
constraint.constant -= 45
// Here I have to set constraint for bottom view and topview of deleted view but I don't know how to do
}
}
}
Here is link to demo project
https://github.com/logictrix/addFieldDemo
Instead of adding each new AvalabileTimeView with its own constraints, use a UIStackView - you can remove almost all of your existing code for adding / removing the new views.
Look at .addArrangedSubview() and .removeArrangedSubview()
Here is some sample code... you'll need to add a UIStackView in Interface Builder, connect it to the Outlet, and adjust the constraints, but that's about all:
// in ViewController.swift
#IBOutlet weak var availableTimeStackView: UIStackView!
#IBAction func addMoreBnt(_ sender: UIButton) {
// instantiate a new AvalabileTimeView
let newView : AvalabileTimeView = AvalabileTimeView()
// set its delegate to self
newView.delegate = self as AvalabileTimeDelegate
// add it to the Stack View
availableTimeStackView.addArrangedSubview(newView)
// standard for auto-layout
newView.translatesAutoresizingMaskIntoConstraints = false
// only constraint needed is Height (width and vertical spacing handled by the Stack View)
newView.heightAnchor.constraint(equalToConstant: 31).isActive = true
}
// new delegate func
func removeMe(_ view: AvalabileTimeView) {
// remove the AvalabileTimeView from the Stack View
availableTimeStackView.removeArrangedSubview(view)
}
// in AvalabileTimeView.swift
protocol AvalabileTimeDelegate{
// don't need this anymore
func txtcheck(text: String!)
// new delegate func
func removeMe(_ view: AvalabileTimeView)
}
#IBAction func deletebnt(_ sender: UIButton) {
delegate.removeMe(self)
}

Move label position in swift with a gesture

I have a label in my view controller. I am trying to move the position of the label 50 points to the left every time I tap the label. This is what I have so far but my label won't move in the simulation. I do have constraints on the label. It is about 100 wide and 50 in height, and it is also centered in the view controller.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let tapGesture = UITapGestureRecognizer(target: gestureLabel, action: #selector(moveLabel))
gestureLabel.addGestureRecognizer(tapGesture)
}
func moveLabel(){
if(left){
moveLeft()
}
else{
moveRight()
}
}
func moveLeft(){
let originalX = gestureLabel.frame.origin.x
if(originalX < 0){
bringIntoFrame()
}
else{
gestureLabel.frame.offsetBy(dx: -50, dy: 0)
}
}
here You can move label where ever You want it
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: self.view)
print(position.x)
print(position.y)
lbl.frame.origin = CGPoint(x:position.x-60,y:position.y-50)
}
}
UILabel's userInteractionEnabled is false by default. Try setting it to true
You should be changing the constraint of the label, not the label itself.
Also, your target should be self, not gestureLabel.
Plus, you could animate that. :)
Like so:
class ViewController: UIViewController {
// Centers the Meme horizontally.
#IBOutlet weak var centerHorizontalConstraint: NSLayoutConstraint!
// A custom UIView.
#IBOutlet weak var okayMeme: OkayMeme!
override func viewDidLoad() {
super.viewDidLoad()
addGestureRecognizer()
}
func addGestureRecognizer() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(moveMeme))
okayMeme.addGestureRecognizer(tapGesture)
}
func moveMeme() {
UIView.animate(withDuration: 2.0) {
self.centerHorizontalConstraint.constant -= 50
self.view.layoutIfNeeded()
}
}
}
"Demo":
You made a new frame, but didn't assign it to the label. offsetBy doesn't change the frame in place. It creates a new one.
Replace
gestureLabel.frame.offsetBy(dx: -50, dy: 0)
with
gestureLabel.frame = gestureLabel.frame.offsetBy(dx: -50, dy: 0)
But the method above assumes, that you're using directly, not constraints. A standard approach is to have a "Leading to Superview" constraint and change its constant instead of changing the frame.
The label wont move because it has constraints. The easiest way to do this is next:
1) create a label programatically (so you can move it freely around)
var labelDinamic = UILabel(frame: CGRectMake(0, 0,200, 200));
labelDinamic.text = "test text";
self.view.addSubview(labelDinamic);
2) set label initial position (i suggest to use the position of your current label that has constraints. also, hide you constraint label, because you dont need it to be displayed)
labelDinamic.frame.origin.x = yourLabelWithConstraints.frame.origin.x;
labelDinamic.frame.origin.y = yourLabelWithConstraints.frame.origin.y;
3) now you move your label where ever You want with the property
labelDinamic.frame.origin.x = 123
labelDinamic.frame.origin.y = 200

Resources