How to remove a view and update constraints? - ios

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

Related

Add/ expand animation will cause unwanted UIScrollView scrolling

I notice that, if I perform add/ expand animation within an UIScrollView, it will cause unwanted scrolling behavior, when the UIScrollView fill with enough content to become scroll-able.
As you can see in the following animation, initially, the add/ expand animation works just fine.
When we have added enough item till the UIScrollView scrollable, whenever a new item is added, and UIScrollView will first perform scroll down, and then scroll up again!
My expectation is that, the UIScrollView should remain static, when add/ expand animation is performed.
Here's the code which performs add/ expand animation.
Add/ expand animation
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
// Clear off horizontal swipe in animation caused by addArrangedSubview
stackView.superview?.layoutIfNeeded()
customView.show()
// Perform expand animation.
UIView.animate(withDuration: 1) {
self.stackView.superview?.layoutIfNeeded()
}
}
Here's the constraint setup of the UIScrollView & added custom view item
Constraint setup
Custom view
class CustomView: UIView {
private var zeroHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.cornerRadius = stackView.frame.height / 2
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
zeroHeightConstraint = self.safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 0)
zeroHeightConstraint.isActive = false
}
func hide() {
zeroHeightConstraint.isActive = true
}
func show() {
zeroHeightConstraint.isActive = false
}
}
Here's the complete source code
https://github.com/yccheok/add-expand-animation-in-scroll-view
Do you have any idea why such problem occur, and we can fix such? Thanks.
Because of the way stack views arrange their subviews, animation can be problematic.
One approach that you may find works better is to embed the stack view in a "container" view.
That way, you can use the .isHidden property when adding an arranged subview, and allow the animation to update the "container" view:
The "add view" function now becomes (I added a Bool so we can skip the animation on the initial add in viewDidLoad()):
func addCustomView(_ animated: Bool) {
let customView = CustomView.instanceFromNib()
stackView.addArrangedSubview(customView)
customView.isHidden = true
if animated {
DispatchQueue.main.async {
UIView.animate(withDuration: 1) {
customView.isHidden = false
}
}
} else {
customView.isHidden = false
}
}
And we can get rid of all of the hide() / show() and zeroHeightConstraint in the custom view class:
class CustomView: UIView {
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
}
override func layoutSubviews() {
super.layoutSubviews()
borderView.layer.cornerRadius = borderView.bounds.height * 0.5
}
}
Since it's a bit difficult to clearly show everything here, I forked your project with the changes: https://github.com/DonMag/add-expand-animation-in-scroll-view
Edit
Another "quirk" of animating a stack view shows up when adding the first arranged subview (also, when removing the last one).
One way to get around that is to add an empty view as the first subview.
So, for this example, in viewDidLoad() before adding an instance of CustomView:
let v = UIView()
stackView.addArrangedSubview(v)
This will make the first arranged subview a zero-height view (so it won't be visible).
Then, if you're implementing removing custom views, just make sure you don't remove that first, empty view.
If your stack view has .spacing = 0 noting else is needed.
If your stack view has a non-zero spacing, add another line:
let v = UIView()
stackView.addArrangedSubview(v)
stackView.setCustomSpacing(0, after: v)
I did a little research on this and the consensus was to update the isHidden and alpha properties when inserting a view with animations.
In CustomView:
func hide() {
alpha = 0.0
isHidden = true
zeroHeightConstraint.isActive = true
}
func show() {
alpha = 1.0
isHidden = false
zeroHeightConstraint.isActive = false
}
In your view controller:
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
self.stackView.layoutIfNeeded()
UIView.animate(withDuration: 00.5) {
customView.show()
self.stackView.layoutIfNeeded()
}
}
Also, the constraints in your storyboard aren't totally correct. You are seeing a red constraint error because autolayout doesn't know the height of your stackView. You can give it a fake height and make sure that "Remove at build time" is checked.
Also, get rid of your scrollView contentView height constraint defined as View.height >= Frame Layout Guide.height. Autolayout doesn't need to know the height, it just needs to know how subviews inside of the contentView stack up to define its vertical content size.
Everything else looks pretty good.

Custom TabBar layout for UITabBarViewController

Please refer to this Answer.
I am trying to do the same thing, however I want to do this in a Tab Bar App where the Now Playing bar is above the Tab Bar in all the scenes of the app.
Update:
I want to have a view at the bottom of the screen (above the tab bar) and under the content views of the different tabs (not above them). In addition, I want to have the ability to remove this view at a certain point making the main view take the whole screen.
I can do this using the mentioned Answer by changing the constraints of the nowPlaying view programmatically.
Using UITabBarViewController subclass it is possible:
Ex:
class DashBoardViewController: UITabBarController {
let nowPlayingBar:UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .blue
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
initView()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
nowPlayingBar.frame = tabBar.frame
}
override func viewDidAppear(_ animated: Bool) {
var newSafeArea = UIEdgeInsets()
// Adjust the safe area to accommodate
// the height of the bottom views.
newSafeArea.bottom += nowPlayingBar.bounds.size.height
// Adjust the safe area insets of the
// embedded child view controller.
self.childViewControllers.forEach({$0.additionalSafeAreaInsets = newSafeArea})
}
private func initView() {
nowPlayingBar.frame = tabBar.frame
view.addSubview(nowPlayingBar)
}
}
You'll add your view/container to your app window, you'd do something like
guard let window = (UIApplication.shared.delegate as? AppDelegate)?.window
else { return } // check if there's a window
let containerHeight: CGFloat = 50 // height for the view where you wish to add the music player
let containerFrame = CGRect(x:0, y: window.frame.maxY - (tabBar.frame.height + containerHeight), width: window.frame.width, height: containerHeight)
// most important part here is the y axis in some sense, you will add the height of the tabBar and the container, then subtract it from window.frame.maxY
let container = UIView(frame: containerFrame)
// now you have the container do whatever you want with it
window.addSubView(container) // finally add the container to window as a subview

How to stack custom views inside a UIStackView

Note: I'm pretty new working with iOS UI.
I want to create a custom view that stacks a custom view inside.
So I created the custom UIStackView
class CustomStackView: UIStackView {
func addItem(color:UIColor){
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "RowView", bundle: bundle)
let rowView = RowView();
let view = nib.instantiate(withOwner: rowView, options: nil).first as! UIView
rowView.addSubview(view)
rowView.view.backgroundColor = color;
addArrangedSubview(rowView)
}
}
class RowView :UIView{
#IBOutlet var view: UIView!
override public var intrinsicContentSize: CGSize {
return CGSize(width: view.frame.width,height:view.frame.height)
}
}
in the RowView.xib I created a simple layout for testing:
Simulated Metrics = Freeform
Height = 100
And the ViewController.swift:
class ViewController: UIViewController {
#IBOutlet weak var customStackView: CustomStackView!
#IBOutlet weak var constraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
customStackView.addItem(color: UIColor.red)
customStackView.addItem(color: UIColor.blue)
customStackView.addItem(color: UIColor.green)
}
#IBAction func click(_ sender: Any) {
constraint.constant = -customStackView.frame.height
UIView.animate(withDuration: 4, animations: {
self.view.layoutIfNeeded();
},completion:nil)
}
}
The result:
The first and second item are displayed correctly but the third is higher than expected.
In addition if I click the button (which should hide the Stackview) keep the "extra" height visible:
How can I fix that?
Edit: Tried the #KristijanDelivuk solution adding a trailing view. And didn't work. Adding cyan color to the view I got this result:
override func viewDidLoad() {
super.viewDidLoad()
customStackView.addItem(color: UIColor.red)
customStackView.addItem(color: UIColor.blue)
customStackView.addItem(color: UIColor.green)
let view = UIView();
view.heightAnchor.constraint(equalToConstant: 50).isActive = true;
view.backgroundColor = UIColor.cyan;
customStackView.addArrangedSubview(view)
}
You can try adding an empty UIView as your last element of UIStackView:
So your hierarchy should look something like this:
- STACKVIEW
-- 1ST ADDED CUSTOM VIEW
-- 2ND ADDED CUSTOM VIEW
-- 3RD ADDED CUSTOM VIEW
-- EMPTY UIVIEW
Empty UIView will take all unallocated space from 3rd view and all should be displayed correctly.
For repositioning button after hiding/showing stackview you can create for example "top constraint" and then on tap change top constraint height to (-) stackview.height or (+) stackview.height - This shouldn't be any problem.

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.

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