Swift iOS -Constraint isn't animating even though view.layoutIfNeeded() is called - ios

I have a rounded likeButton that is behind a rounded moreButton. The likeButton is pinned to the centerX and centerY of the moreButton. When I press the moreButton I want to animate the likeButton 200 points above the moreButton.
I use a NSLayoutConstraint to keep track of the likeButton's centerY and to make changes to it. When I call UIView.animate and inside it's closure I call self.view.layoutIfNeeded() the view isn't updating.
#objc func moreButtonTapped(){
likeButtonCenterY?.isActive = false
likeButton.centerYAnchor.constraint(equalTo: self.moreButton.centerYAnchor, constant: -200)
likeButtonCenterY?.isActive = true
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
Where am I going wrong?
let moreButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(named: "more"), for: .normal)
button.tintColor = .red
button.clipsToBounds = true
button.addTarget(self, action: #selector(moreButtonTapped), for: .touchUpInside)
return button
}()
let likeButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(named: "like"), for: .normal)
button.tintColor = .blue
button.clipsToBounds = true
return button
}()
var likeButtonCenterY: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setAnchors()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
moreButton.layer.cornerRadius = moreButton.frame.size.width / 2
likeButton.layer.cornerRadius = likeButton.frame.size.width / 2
}
#objc func moreButtonTapped(){
likeButtonCenterY?.isActive = false
likeButton.centerYAnchor.constraint(equalTo: self.moreButton.centerYAnchor, constant: -200)
likeButtonCenterY?.isActive = true
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
func setAnchors(){
view.addSubview(likeButton)
view.addSubview(moreButton)
moreButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
moreButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -200).isActive = true
moreButton.widthAnchor.constraint(equalToConstant: 100).isActive = true
moreButton.heightAnchor.constraint(equalToConstant: 100).isActive = true
likeButtonCenterY = likeButton.centerYAnchor.constraint(equalTo: moreButton.centerYAnchor)
likeButtonCenterY?.isActive = true
likeButton.centerXAnchor.constraint(equalTo: moreButton.centerXAnchor).isActive = true
likeButton.widthAnchor.constraint(equalToConstant: 100).isActive = true
likeButton.heightAnchor.constraint(equalToConstant: 100).isActive = true
}

Try this
#objc func moreButtonTapped(){
likeButtonCenterY?.isActive = false
likeButtonCenterY = likeButton.centerYAnchor.constraint(equalTo: self.moreButton.centerYAnchor, constant: -200)
likeButtonCenterY?.isActive = true
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
You have forgotten to write likeButtonCenterY =

If you are activating/deactivating multiple constraints at the same time (like a batch), you might need to override the updateConstraints() method. More information is here: https://developer.apple.com/documentation/uikit/uiview/1622512-updateconstraints
Basically your code in the moreButtonTapped() will go in the overridden implementation of the updateConstraints() method. Don't forget to call super() in the last line of the method.
Then your animation code should look like look like:
UIView.animate(withDuration: 0.3) {
self.view.setNeedsUpdateConstraints()
self.view.layoutIfNeeded()
})

Related

iOS Swift protocol & delegate

Why my delegation is not working?
With my sample the action button works when clicked, but for some reason it does not reach the didAction function in my second controller.
protocol HomeControllerDelegate: class {
func didAction()
}
class HomeController: UIViewController {
weak var delegate: HomeControllerDelegate?
private let actionButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Action", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .black
button.setHeight(50)
button.setWidth(100)
button.addTarget(self, action: #selector(handleAction), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(actionButton)
actionButton.centerX(inView: view)
actionButton.centerY(inView: view)
}
#objc func handleAction() {
print("DEBUG: Handle Action Button delegate here....")
delegate?.didAction()
}
}
class SecondController: UIViewController {
let homeController = HomeController()
override func viewDidLoad() {
super.viewDidLoad()
homeController.delegate = self
}
}
extension SecondController: HomeControllerDelegate {
func didAction() {
print("DEBUG: In SecondController - didAction()")
}
}
Many thanks for the guidance, it has made me look at the fundamentals and also what to research.
I have created the below which solves my problem.
A container controller that Instantiates two ViewControlers and sets them as childVC's
Then created a protocol on FirstChildVC and SecondChildVC is the delegate
All is working now an I understand a lot better.
class ContainerController: UIViewController {
let secondChildVC = SecondChildVC()
let firstChildVC = FirstChildVC()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
addFirstChildVC()
addSecondChildVC()
}
func addSecondChildVC(){
addChild(secondChildVC)
view.addSubview(secondChildVC.view)
secondChildVC.didMove(toParent: self)
setSecondChildVCConstraints()
}
func addFirstChildVC(){
addChild(firstChildVC)
view.addSubview(firstChildVC.view)
firstChildVC.delegateActionButton = secondChildVC
firstChildVC.didMove(toParent: self)
setFirstChildVCConstraints()
}
func setFirstChildVCConstraints() {
firstChildVC.view.translatesAutoresizingMaskIntoConstraints = false
firstChildVC.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
firstChildVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
firstChildVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
firstChildVC.view.heightAnchor.constraint(equalToConstant: 200).isActive = true
}
func setSecondChildVCConstraints() {
secondChildVC.view.translatesAutoresizingMaskIntoConstraints = false
secondChildVC.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive = true
secondChildVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
secondChildVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
secondChildVC.view.heightAnchor.constraint(equalToConstant: 200).isActive = true
}
}
protocol FirstChildVCDelegate: class {
func didAction(data: String)
}
class FirstChildVC: UIViewController {
weak var delegateActionButton: FirstChildVCDelegate!
private let actionButton: UIButton = {
let button = UIButton(type: .system)
let buttonHeight = 30
button.setTitle("Button", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .black
button.layer.cornerRadius = CGFloat(buttonHeight / 2)
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: CGFloat(buttonHeight)).isActive = true
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: 100).isActive = true
button.addTarget(self, action: #selector(handleAction), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemPink
configureActionButtonView()
}
func configureActionButtonView() {
view.addSubview(actionButton)
actionButton.translatesAutoresizingMaskIntoConstraints = false
actionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
actionButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
#objc func handleAction() {
delegateActionButton.didAction(data: "Button Pressed")
}
}
class SecondChildVC: UIViewController {
private let actionButtonLabel: UILabel = {
let label = UILabel()
label.text = "test"
label.font = .systemFont(ofSize: 18)
label.textColor = .white
label.isHidden = true
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemPurple
configureActionButtonLabel()
}
func configureActionButtonLabel() {
view.addSubview(actionButtonLabel)
actionButtonLabel.translatesAutoresizingMaskIntoConstraints = false
actionButtonLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
actionButtonLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
}
extension SecondChildVC: FirstChildVCDelegate {
func didAction(data: String) {
actionButtonLabel.isHidden.toggle()
actionButtonLabel.text = data
}
}
Initial State
State once Button pressed

Updating layout anchors not working as expected

I have created a view with height 100 using NSLayout anchors. When I'm trying to update that on button click, it's not working.
I have tried below code, but it's not working.
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
let viewAnimate = UIView()
var isHidden = false
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(viewAnimate)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewAnimate.translatesAutoresizingMaskIntoConstraints = false
viewAnimate.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8).isActive = true
viewAnimate.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8).isActive = true
viewAnimate.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100).isActive = true
viewAnimate.heightAnchor.constraint(equalToConstant: 100).isActive = true
viewAnimate.backgroundColor = UIColor.red
}
#IBAction func show() {
if !isHidden {
viewAnimate.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 200).isActive = true
button.setTitle("Show", for: .normal)
} else {
button.setTitle("Hide", for: .normal)
viewAnimate.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100).isActive = true
}
UIView.animate(withDuration: 1) {
self.viewAnimate.layoutIfNeeded()
}
isHidden = !isHidden
}
}
View should change the height based on height constraint
Your current code creates conflicts as every line like viewAnimate.topAnchor.constraint(equalTo: adds a new constraint , create a var
var topCon:NSLayoutConstraint!
topCon = viewAnimate.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100)
topCon.isActive = true
#IBAction func show() {
if !isHidden {
topCon.constant = 200
button.setTitle("Show", for: .normal)
} else {
button.setTitle("Hide", for: .normal)
topCon.constant = 100
}
UIView.animate(withDuration: 1) {
self.view.layoutIfNeeded()
}
isHidden = !isHidden
}

UIDatePicker and Two Buttons from different time

Start app.
Choice of hour and secondButton. I want different hour.
I have two buttons and one DatePicker. set datePickerMode = .time.
I need to set two different hours on different buttons.
Unfortunately, I could not find solutions for this.
This is my code:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(firstButton)
firstButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100).isActive = true
firstButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 30).isActive = true
firstButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -30).isActive = true
firstButton.heightAnchor.constraint(equalToConstant: 100).isActive = true
view.addSubview(secondButton)
secondButton.topAnchor.constraint(equalTo: firstButton.bottomAnchor, constant: 10).isActive = true
secondButton.leftAnchor.constraint(equalTo: firstButton.leftAnchor).isActive = true
secondButton.rightAnchor.constraint(equalTo: firstButton.rightAnchor).isActive = true
view.addSubview(myDatePicker)
myDatePicker.topAnchor.constraint(equalTo: secondButton.bottomAnchor, constant: 10).isActive = true
myDatePicker.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 10).isActive = true
myDatePicker.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10).isActive = true
}
// MyFirstButton
let firstButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("First", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.tintColor = .white
button.tag = 0
button.addTarget(self, action: #selector(handleButton), for: .touchUpInside)
return button
}()
// MySecondButton
let secondButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Second", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.tintColor = .white
button.tag = 1
button.addTarget(self, action: #selector(handleButton), for: .touchUpInside)
return button
}()
// Target First and Second Button
#objc func handleButton() {
view.layoutIfNeeded()
UIView.animate(withDuration: 0.25, animations: {
self.view.layoutIfNeeded()
}) { (completed) in
UIView.animate(withDuration: 0.25, animations: {
self.myDatePicker.alpha = 1
self.view.layoutIfNeeded()
})
}
}
// DataPicker
let myDatePicker: UIDatePicker = {
let pv = UIDatePicker()
pv.timeZone = .current
pv.datePickerMode = .time
pv.backgroundColor = .white
pv.translatesAutoresizingMaskIntoConstraints = false
pv.addTarget(self, action: #selector(handleMyDatePicker), for: .valueChanged)
return pv
}()
// Target
#objc func handleMyDatePicker(sender: UIDatePicker) {
let formatter = DateFormatter()
formatter.timeStyle = .short
let selectedTime = formatter.string(from: sender.date)
firstButton.setTitle(selectedTime, for: .normal)
// here is the problem
secondButton.setTitle(selectedTime, for: .normal)
myDatePicker.alpha = 0
}
How to achieve this effect?
Set your myDatePicker tag to same with button, that you touched
#objc func handleButton(button: UIButton) {
view.layoutIfNeeded()
UIView.animate(withDuration: 0.25, animations: {
self.view.layoutIfNeeded()
}) { (completed) in
UIView.animate(withDuration: 0.25, animations: {
self.myDatePicker.alpha = 1
self.myDatePicker.tag = button.tag
self.view.layoutIfNeeded()
})
}
}
#objc func handleMyDatePicker(sender: UIDatePicker) {
let formatter = DateFormatter()
formatter.timeStyle = .short
let selectedTime = formatter.string(from: sender.date)
let button = self.view.viewWithTag(sender.tag) as? UIButton
button?.setTitle(selectedTime, for: .normal)
myDatePicker.alpha = 0
}
and small change in your addTarget code
button.addTarget(self, action: #selector(handleButton(button:)), for: .touchUpInside)

Swift: Screen Freezing from time to time when Dismissing a ViewController displaying a picture over the current context

I am having some troubles with: The screen freezing from time to time when Dismissing the ViewController displaying a picture over the current context.
May someone provide me some insights on how to fix this problem?
A sample of my codes is found below:
import UIKit
class ViewControllerCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
addSubview(showPhotoButton)
showPhotoButton.leftAnchor.constraint(equalTo: leftAnchor, constant: 200).isActive = true
showPhotoButton.bottomAnchor.constraint(equalTo: topAnchor, constant: 160).isActive = true
showPhotoButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
showPhotoButton.widthAnchor.constraint(equalToConstant: 70).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var showPhotoButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Show", for: .normal)
button.addTarget(self, action: #selector(showSale), for: .touchUpInside)
button.setTitleColor(UIColor(r: 120, g: 80, b: 255), for: .normal)
return button
}()
#objc func showSale() {
let popupViewController = PopupViewController()
popupViewController.modalPresentationStyle = .overCurrentContext
popupViewController.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
window!.rootViewController?.present(PopupViewController, animated: true, completion: nil)
}
}
import UIKit
class SalePopupViewController: UIViewController {
override func viewDidLoad() {
view.backgroundColor = UIColor(white: 1, alpha: 0.60)
view.isOpaque = false
view.addSubview(rebateImage)
view.addSubview(dismissButton)
dismissButton.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
dismissButton.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
dismissButton.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
dismissButton.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
rebateImage.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
rebateImage.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -35).isActive = true
rebateImage.heightAnchor.constraint(equalToConstant: 290).isActive = true
rebateImage.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1).isActive = true
}
let dismissButton: UIButton = {
let button = UIButton(type: .system)
button.addTarget(self, action: #selector(dismissPopup), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
let rebateImage: UIImageView = {
let image = UIImageView()
image.translatesAutoresizingMaskIntoConstraints = false
image.layer.masksToBounds = false
image.layer.cornerRadius = 2
image.contentMode = .scaleAspectFill
image.clipsToBounds = true
image.image = UIImage(named: "SaleCostco")
return image
}()
#objc func dismissPopup() {
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.main.async {
self.dismiss(animated: true, completion: nil)
}
}
}
}
Your asynchronous code doesn't make sense. You dispatch onto a global queue and then immediately back onto the main thread. Try simply changing the implementation of dismissPopup() to this:
#objc func dismissPopup() {
dismiss(animated: true, completion: nil)
}

Arranging buttons programmatically with constraints

I have an array of buttons that I am iterating through and adding the buttons onto the view. Each button should be adjacent to the previous button, so I'm setting the leading constraint to the previous button's trailing. But the buttons end up layered on top of each other with only the top one displayed.
for k in 0 ..< buttons.count {
view.addSubview(buttons[k])
if k > 0 {
buttons[k].leadingAnchor.constraint(equalTo: buttons[k-1].trailingAnchor).isActive = true
}
}
Edit:
I don't know if this is part of the problem, but here's how I'm creating the buttons. I set each to (0,0) because I don't know where they'll end up. I assume the constraint would reposition them as needed (first time use programmatic constraints).
let size = CGRect(x: 0, y: 0, width: buttonWidth, height: buttonHeight)
let button: UIButton = UIButton(frame: size)
Here a simple playground that works with a UIStackView. You can play a bit and accommodate for your goal.
UIStackViews are very flexible components if you want avoid creating constraints manually.
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController: UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let buttons = createButtons()
let stackView = createStackView(with: UILayoutConstraintAxis.vertical)
buttons.forEach { button in
stackView.addArrangedSubview(button)
}
view.addSubview(stackView)
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
self.view = view
}
func createStackView(with layout: UILayoutConstraintAxis) -> UIStackView {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = layout
stackView.distribution = .equalSpacing
stackView.spacing = 0
return stackView
}
func createButtons() -> [UIButton] {
var buttons = [UIButton]()
for x in 0..<5 {
let button = UIButton(type: .custom)
button.backgroundColor = .red
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: 50).isActive = true
button.heightAnchor.constraint(equalToConstant: 100).isActive = true
button.setTitle("Title \(x)", for: .normal)
buttons.append(button)
}
return buttons
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
The key problem is you should use isActive to active constraint.
The following is example
var buttons: [UIButton] = []
for index in 0...5 {
let button = UIButton(frame: .zero)
button.setTitleColor(.black, for: .normal)
button.setTitle("button \(index)", for: .normal)
button.layer.borderColor = UIColor.gray.cgColor
button.layer.borderWidth = 1.0
button.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(button)
buttons.append(button)
}
for index in 0...5 {
let button = buttons[index]
if index == 0 {
button.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8.0).isActive = true
button.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 20.0).isActive = true
} else {
let preButton = buttons[index - 1]
button.leadingAnchor.constraint(equalTo: preButton.trailingAnchor, constant: 8.0).isActive = true
button.topAnchor.constraint(equalTo: preButton.topAnchor, constant: 0.0).isActive = true
}
}

Resources