Simple slide up animation on UIView using NSLayoutConstraints - ios

I have spent a while trying to figure this out, but I just can't. There is probably something really simple I am missing here. I am trying to animate a view coming in from the bottom. This si the code I am using for the view:
private let undoView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.layer.cornerRadius = 15
view.layer.shadowOffset = .zero
view.layer.shadowOpacity = 0.3
view.layer.shadowRadius = 5
view.layer.shouldRasterize = true
view.layer.rasterizationScale = UIScreen.main.scale
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Undo", for: .normal)
button.titleLabel?.textAlignment = .center
button.setTitleColor(.systemBlue, for: .normal)
let buttonConstraints = [
button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
button.topAnchor.constraint(equalTo: view.topAnchor, constant: 16),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16)
]
view.addSubview(button)
NSLayoutConstraint.activate(buttonConstraints)
let swipeGesture = UISwipeGestureRecognizer(target: view, action: #selector(undoViewSwiped))
view.addGestureRecognizer(swipeGesture)
return view
}()
And this is the code I am using to try and achieve the animation:
func addUndoView() {
var bottomConstraint = undoView.topAnchor.constraint(equalTo: view.bottomAnchor, constant: 0)
let constraints = [
undoView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
undoView.widthAnchor.constraint(equalToConstant: view.bounds.width / 3),
bottomConstraint,
undoView.heightAnchor.constraint(equalToConstant: 50)
]
view.addSubview(undoView)
NSLayoutConstraint.activate(constraints)
undoView.layoutIfNeeded()
bottomConstraint.isActive = false
bottomConstraint = undoView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: -58)
bottomConstraint.isActive = true
UIView.animate(withDuration: 0.5, delay: 0.2, animations: {
self.undoView.layoutIfNeeded()
})
}
I want the view to just be created underneath the visible view, and then slide up to 8 above the bottom layout margin. This code however just makes the view 'expand' at its final position instead of coming into view from the bottom. Using self.view.layoutIfNeeded() makes the view fly in from the top left of the screen, for some reason.

good day I usually work with transform property for move one view from x position to y portion, please check this example.
class ViewController: UIViewController {
let button : UIButton = {
let button = UIButton(type: .custom)
button.setTitle("Button", for: .normal)
button.backgroundColor = .gray
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
let customView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(button)
view.addSubview(customView)
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
customView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
customView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
customView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
customView.heightAnchor.constraint(equalToConstant: 100)
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupAnimationCustomView()
}
private func setupAnimationCustomView(){
UIView.animate(withDuration: 1, delay: 0 , options: .curveEaseOut, animations: {
print(self.button.frame.origin.y)
let translationY = self.view.frame.height - self.button.frame.origin.y - self.button.frame.height - self.customView.frame.height - 8
self.customView.transform = CGAffineTransform(translationX: 0, y: translationY * (-1))
}, completion: nil)
}
}
on the animation I calculate the distance that I need to move my customView.

Related

Collapsed navigation bar title is unintentionally expanded to large title if any UI element is modified

To demonstrate the problem, I wrote a very simple demo app. If I scroll down on the scroll view, the navigation bar title collapses as desired. If the button on the bottom is pressed, the button image is changed. However, this also causes the navigation bar to show a large title again, which is not desired. The same problem also occurs if for example the visibility of a UI element is changed (button.isHidden = true), or if any other UI element is modified in a number of other ways.
Is there any possibility to avoid this undesired behaviour, i.e., for the title to remain in its collapsed state even if UI elements are modified?
Code:
import UIKit
class ViewController: UIViewController {
var buttonActivated = false
let scrollView: UIScrollView = {
let view = UIScrollView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .clear
return view
}()
let cardView1 : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
view.layer.cornerRadius = 12.5
view.layer.shadowOffset = CGSize(width: 0, height: 5)
view.layer.shadowRadius = 5
view.layer.shadowOpacity = 0.3
return view
}()
let cardView2 : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
view.layer.cornerRadius = 12.5
view.layer.shadowOffset = CGSize(width: 0, height: 5)
view.layer.shadowRadius = 5
view.layer.shadowOpacity = 0.3
return view
}()
let cardView3 : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
view.layer.cornerRadius = 12.5
view.layer.shadowOffset = CGSize(width: 0, height: 5)
view.layer.shadowRadius = 5
view.layer.shadowOpacity = 0.3
return view
}()
let cardView4 : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
view.layer.cornerRadius = 12.5
view.layer.shadowOffset = CGSize(width: 0, height: 5)
view.layer.shadowRadius = 5
view.layer.shadowOpacity = 0.3
return view
}()
let button: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Test Button ", for: .normal)
button.setTitleColor(UIColor.label, for: .normal)
button.setImage(UIImage(systemName: "chevron.down"), for: .normal)
button.tintColor = UIColor.label
button.titleLabel?.font = .systemFont(ofSize: 12, weight: .regular)
button.contentHorizontalAlignment = .center
button.semanticContentAttribute = .forceRightToLeft
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
scrollView.addSubview(cardView1)
scrollView.addSubview(cardView2)
scrollView.addSubview(cardView3)
scrollView.addSubview(cardView4)
scrollView.addSubview(button)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
cardView1.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 20),
cardView1.leadingAnchor.constraint(equalTo: scrollView.readableContentGuide.leadingAnchor, constant: 20),
cardView1.trailingAnchor.constraint(equalTo: scrollView.readableContentGuide.trailingAnchor, constant: -20),
cardView1.heightAnchor.constraint(equalToConstant: 300),
cardView2.topAnchor.constraint(equalTo: cardView1.bottomAnchor, constant: 20),
cardView2.leadingAnchor.constraint(equalTo: scrollView.readableContentGuide.leadingAnchor, constant: 20),
cardView2.trailingAnchor.constraint(equalTo: scrollView.readableContentGuide.trailingAnchor, constant: -20),
cardView2.heightAnchor.constraint(equalToConstant: 300),
cardView3.topAnchor.constraint(equalTo: cardView2.bottomAnchor, constant: 20),
cardView3.leadingAnchor.constraint(equalTo: scrollView.readableContentGuide.leadingAnchor, constant: 20),
cardView3.trailingAnchor.constraint(equalTo: scrollView.readableContentGuide.trailingAnchor, constant: -20),
cardView3.heightAnchor.constraint(equalToConstant: 300),
cardView4.topAnchor.constraint(equalTo: cardView3.bottomAnchor, constant: 20),
cardView4.leadingAnchor.constraint(equalTo: scrollView.readableContentGuide.leadingAnchor, constant: 20),
cardView4.trailingAnchor.constraint(equalTo: scrollView.readableContentGuide.trailingAnchor, constant: -20),
cardView4.heightAnchor.constraint(equalToConstant: 300),
button.topAnchor.constraint(equalTo: cardView4.bottomAnchor, constant: 20),
button.leadingAnchor.constraint(equalTo: scrollView.readableContentGuide.leadingAnchor, constant: 20),
button.trailingAnchor.constraint(equalTo: scrollView.readableContentGuide.trailingAnchor, constant: -20),
button.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -20),
])
}
#objc private func buttonPressed() {
if !buttonActivated {
button.setImage(UIImage(systemName: "chevron.up"), for: .normal)
}
else {
button.setImage(UIImage(systemName: "chevron.down"), for: .normal)
}
buttonActivated = !buttonActivated
}
}
Edit: The same problem is present if the UI change is performed from the main thread, e.g.:
DispatchQueue.main.async {
self.button.setImage(UIImage(systemName: "chevron.down"), for: .normal)
}

UIView.animateKeyFrames blocks Uibutton click - view hierarchy issue or nsLayoutConstraint?

I currently have a ToastView class that creates a view that contains a button, label, and image. The goal is to get this ToastView instance to slide up and down on the controller as a subview, like a notification bar. In LayoutOnController(), I add the bar below the visible part of the page using nslayoutconstraint(), using the margins of the superview passed in. Finally, I animate upwards using keyframes and transform the view upwards.
My problem is that once I animate the bar upwards, the button becomes non-interactive. I can click my button and trigger my #objc function if I only call LayoutOnController().
Im assuming my problem is either in the ToastViewController where I add the ToastView object as a subview (maybe misunderstanding my view hierarchy?), or that the NSLayoutConstraints do not behave well with UIView.AnimateKeyFrames. I have tried using layout constants
( self.bottom.constant = 50) instead of self?.transform = CGAffineTransform(translationX: 0, y: -40) , but the view doesnt show at all if I do that. Ive been stuck for a while so any insight is appreciated!
Here is my code:
import UIKit
import Alamofire
import CoreGraphics
class ToastView: UIView {
let toastSuperviewMargins: UILayoutGuide
let toastRenderType: String
var top = NSLayoutConstraint()
var bottom = NSLayoutConstraint()
var width = NSLayoutConstraint()
var height = NSLayoutConstraint()
var trailing = NSLayoutConstraint()
var leading = NSLayoutConstraint()
private let label: UILabel = {
let label = UILabel()
label.textAlignment = .left
label.font = .systemFont(ofSize: 15)
label.textColor = UIColor.white
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let dismissButton: UIButton = {
let dismissButton = UIButton( type: .custom)
dismissButton.isUserInteractionEnabled = true
dismissButton.setTitleColor( UIColor.white, for: .normal)
dismissButton.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .semibold)
dismissButton.translatesAutoresizingMaskIntoConstraints = false
return dismissButton
}()
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
init(toastRenderType: String, toastController: UIViewController, frame: CGRect) {
self.toastSuperviewMargins = toastController.view.layoutMarginsGuide
self.toastRenderType = toastRenderType
super.init(frame: frame)
configureToastType()
layoutToastConstraints()
}
//animate upwards from bottom of screen position (configured in layoutOnController() )
//CANNOT CLICK BUTTON HERE
func animateToast(){
UIView.animateKeyframes(withDuration: 4.6, delay: 0.0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.065, animations: { [weak self] in
self?.transform = CGAffineTransform(translationX: 0, y: -40)
})
UIView.addKeyframe(withRelativeStartTime: 0.93, relativeDuration: 0.065, animations: { [weak self] in
self?.transform = CGAffineTransform(translationX: 0, y: 50)
})
}) { (completed) in
self.removeFromSuperview()
}
}
//configure internal text and color scheme
func configureToastType(){
if self.toastRenderType == "Ok" {
self.backgroundColor = UIColor(hue: 0.4222, saturation: 0.6, brightness: 0.78, alpha: 1.0)
label.text = "Configuration saved!"
dismissButton.setTitle("OK", for: .normal)
imageView.image = UIImage(named: "checkmark.png")
}
else{
self.backgroundColor = UIColor(red: 0.87, green: 0.28, blue: 0.44, alpha: 1.00)
label.text = "Configuration deleted."
dismissButton.setTitle("Undo", for: .normal)
dismissButton.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .semibold)
imageView.image = UIImage(named: "close2x.png")
}
dismissButton.addTarget(self, action: #selector(clickedToast), for: .touchUpInside)
}
//layout widget on the controller,using margins passed in via controller. start widget on bottom of screen.
func layoutOnController(){
let margins = self.toastSuperviewMargins
self.top = self.topAnchor.constraint(equalTo: margins.bottomAnchor, constant: 0)
self.width = self.widthAnchor.constraint(equalTo: margins.widthAnchor, multiplier: 0.92)
self.height = self.heightAnchor.constraint(equalToConstant: 48)
self.leading = self.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 16)
self.trailing = self.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 16)
NSLayoutConstraint.activate([
self.top,
self.width,
self.height,
self.leading,
self.trailing
])
}
//layout parameters internal to widget as subviews
func layoutToastConstraints(){
self.translatesAutoresizingMaskIntoConstraints = false
layer.masksToBounds = true
layer.cornerRadius = 8
addSubview(label)
addSubview(imageView)
addSubview(dismissButton)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 48),
label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -69),
label.heightAnchor.constraint(equalToConstant: 20),
label.widthAnchor.constraint(equalToConstant: 226),
label.topAnchor.constraint(equalTo: self.topAnchor, constant: 14),
label.bottomAnchor.constraint(equalTo: self.topAnchor, constant: -14),
imageView.heightAnchor.constraint(equalToConstant: 20),
imageView.widthAnchor.constraint(equalToConstant: 20),
imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 27),
imageView.trailingAnchor.constraint(equalTo: label.leadingAnchor, constant: -10.33),
imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 20.5),
imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -20.17),
dismissButton.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 24),
dismissButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16),
dismissButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 16),
dismissButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -16),
])
self.layoutIfNeeded()
}
#objc func clickedToast(){
print("you clicked the toast button")
self.removeFromSuperview()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
ToastViewController, where I am testing the bar animation:
import UIKit
import CoreGraphics
class ToastViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.backgroundColor = .white
toastTest()
}
//the toastRenderType specifies whether the notif bar is red or green
func toastTest() -> () {
let toastView = ToastView(toastRenderType: "Ok", toastController: self, frame: .zero)
view.addSubview(toastView)
toastView.layoutOnController() //button click works
toastView.animateToast() //button click ignored
}
}
You cannot tap the button because you've transformed the view. So the button is still below the bottom - only its visual representation is visible.
You can either implement hitTest(...), calculate if the touch location is inside the transformed button, and call clickedToast() if so, or...
What I would recommend:
func animateToast(){
// call this async on the main thread
// so auto-layout has time to set self's initial position
DispatchQueue.main.async { [weak self] in
guard let self = self, let sv = self.superview else { return }
// decrement the top constant by self's height + 8 (for a little spacing below)
self.top.constant -= (self.frame.height + 8)
UIView.animate(withDuration: 0.5, delay: 0.0, options: [], animations: {
sv.layoutIfNeeded()
}, completion: { b in
if b {
// animate back down after 4-seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 4.0, execute: { [weak self] in
// we only want to execute this if the button was not tapped
guard let self = self, let sv = self.superview else { return }
// set the top constant back to where it was
self.top.constant += (self.frame.height + 8)
UIView.animate(withDuration: 0.5, delay: 0.0, options: [], animations: {
sv.layoutIfNeeded()
}, completion: { b in
self.removeFromSuperview()
})
})
}
})
}
}

UIView.animatekeyframes skips all keyframes except first

My goal is to have my UIview notification slide up from the bottom of the controller page, and then slide down again after a few seconds.
Inside the animateUp() func, I use UIView.animateKeyFrames function to add 2 keyframes. One keyframe brings the view into visibility, and the second brings it back down to where it started. When I execute with only the first keyframe added,the view visibly moves. However, when I add the second keyframe, the view does not animate and only shows itself at the endpoint. I have made sure that the relative duration and relative start times are between 0 and 1 as the documentation mentions. I have also tried to use uiview.animate(withduration:) as an alternative but get the same behavior there.
Code below (skip to bottom for just the animateUp() fn:
import UIKit
import Alamofire
import CoreGraphics
class ToastView: UIView {
let toastVM: ToastViewModel
let toastController: UIViewController
//constraints to specify layout on controller
var top = NSLayoutConstraint()
var bottom = NSLayoutConstraint()
var width = NSLayoutConstraint()
var height = NSLayoutConstraint()
var trailing = NSLayoutConstraint()
var leading = NSLayoutConstraint()
private let label: UILabel = {
let label = UILabel()
label.textAlignment = .left
label.font = .systemFont(ofSize: 15)
label.textColor = UIColor.white
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let dismissButton = UIButton()
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
init(toastVM: ToastViewModel, toastController: UIViewController, frame: CGRect) {
self.toastVM = toastVM
self.toastController = toastController
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
config()
layoutToastConstraints()
}
//layout parameters internal to widget
func layoutToastConstraints(){
layer.masksToBounds = true
layer.cornerRadius = 8
addSubview(label)
addSubview(imageView)
addSubview(dismissButton)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 48),
label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -69),
label.heightAnchor.constraint(equalToConstant: 20),
label.widthAnchor.constraint(equalToConstant: 226),
label.topAnchor.constraint(equalTo: self.topAnchor, constant: 14),
label.bottomAnchor.constraint(equalTo: self.topAnchor, constant: -14),
imageView.heightAnchor.constraint(equalToConstant: 7.33), //change to 7.33 later
imageView.widthAnchor.constraint(equalToConstant: 10.67), //10.67
imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 27),
imageView.trailingAnchor.constraint(equalTo: label.leadingAnchor, constant: -10.33),
imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 20.5),
imageView.topAnchor.constraint(equalTo: label.topAnchor),
imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -20.17),
dismissButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 298),
dismissButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 16),
dismissButton.bottomAnchor.constraint(equalTo: self.topAnchor, constant: -16),
dismissButton.heightAnchor.constraint(equalToConstant: 16),
dismissButton.widthAnchor.constraint(equalToConstant: 21)
])
}
//layout widget on the controller, using margins passed in. start widget on bottom of screen.
func layoutOnController(){
self.translatesAutoresizingMaskIntoConstraints = false
let margins = self.toastController.view.layoutMarginsGuide
self.top = self.topAnchor.constraint(equalTo: margins.bottomAnchor, constant: 0)
self.width = self.widthAnchor.constraint(equalTo: margins.widthAnchor, multiplier: 0.92)
self.height = self.heightAnchor.constraint(equalToConstant: 48)
self.leading = self.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 16)
self.trailing = self.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 16)
NSLayoutConstraint.activate([
self.top,
self.width,
self.height,
self.leading,
self.trailing
])
}
func animateUp(){
UIView.animateKeyframes(withDuration: 1.0, delay: 0.0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.50, animations: {
self.transform = CGAffineTransform(translationX: 0, y: -50)
})
UIView.addKeyframe(withRelativeStartTime: 0.50, relativeDuration: 0.50, animations: {
self.transform = CGAffineTransform(translationX: 0, y: 0)
})
}) { (completed) in
print("done")
}
}
EDIT:Based on your comment, I have now realized the issue is most likely my ViewController. I have done some refactoring and no longer use a 'ToastViewModel' (which was an unnecessary class used to hold data) Below is my controller and updated code.
import UIKit
class ToastViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
toastTest()
}
//the toastRenderType specifies whether the notif bar is red or green
func toastTest() -> () {
let toastView = ToastView(toastRenderType: "Ok",toastController: self, frame: .zero)
view.addSubview(toastView)
toastView.layoutOnController()
toastView.animateUp()
}
}
Updated View:
import UIKit
import Alamofire
import CoreGraphics
class ToastView: UIView {
let toastSuperviewMargins: UILayoutGuide
let toastRenderType: String
var top = NSLayoutConstraint()
var bottom = NSLayoutConstraint()
var width = NSLayoutConstraint()
var height = NSLayoutConstraint()
var trailing = NSLayoutConstraint()
var leading = NSLayoutConstraint()
private let label: UILabel = {
let label = UILabel()
label.textAlignment = .left
label.font = .systemFont(ofSize: 15)
label.textColor = UIColor.white
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let dismissButton: UIButton = {
let dismissButton = UIButton()
dismissButton.addTarget(target: self, action: #selector(self.clickedToast), for: .allTouchEvents)
dismissButton.addTarget(ToastView.self, action: #selector(clickedToast), for: .allTouchEvents)
dismissButton.isUserInteractionEnabled = true
dismissButton.isOpaque = true
dismissButton.setTitleColor( UIColor.white, for: .normal)
dismissButton.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .semibold)
return dismissButton
}()
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
init(toastRenderType: String, toastController: UIViewController, frame: CGRect) {
self.toastSuperviewMargins = toastController.view.layoutMarginsGuide
self.toastRenderType = toastRenderType
super.init(frame: frame)
configureToastType()
layoutToastConstraints()
}
//layout parameters internal to widget
func layoutToastConstraints(){
self.translatesAutoresizingMaskIntoConstraints = false
layer.masksToBounds = true
layer.cornerRadius = 8
addSubview(label)
addSubview(imageView)
addSubview(dismissButton)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 48),
label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -69),
label.heightAnchor.constraint(equalToConstant: 20),
label.widthAnchor.constraint(equalToConstant: 226),
label.topAnchor.constraint(equalTo: self.topAnchor, constant: 14),
label.bottomAnchor.constraint(equalTo: self.topAnchor, constant: -14),
imageView.heightAnchor.constraint(equalToConstant: 7.33),
imageView.widthAnchor.constraint(equalToConstant: 10.67),
imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 27),
imageView.trailingAnchor.constraint(equalTo: label.leadingAnchor, constant: -10.33),
imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 20.5),
imageView.topAnchor.constraint(equalTo: label.topAnchor),
imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -20.17),
dismissButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 298),
dismissButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0),
dismissButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 16),
dismissButton.bottomAnchor.constraint(equalTo: self.topAnchor, constant: -16),
dismissButton.heightAnchor.constraint(equalToConstant: 16),
dismissButton.widthAnchor.constraint(equalToConstant: 21)
])
self.layoutIfNeeded()
}
//layout widget on the controller,using margins passed in. start widget on bottom of screen.
func layoutOnController(){
self.translatesAutoresizingMaskIntoConstraints = false
let margins = self.toastSuperviewMargins
self.top = self.topAnchor.constraint(equalTo: margins.bottomAnchor, constant: 0)
self.width = self.widthAnchor.constraint(equalTo: margins.widthAnchor, multiplier: 0.92)
self.height = self.heightAnchor.constraint(equalToConstant: 48)
self.leading = self.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 16)
self.trailing = self.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 16)
NSLayoutConstraint.activate([
self.top,
self.width,
self.height,
self.leading,
self.trailing
])
}
//now using a longer duration
func animateUp(){
UIView.animateKeyframes(withDuration: 10.0, delay: 0.0, options: .allowUserInteraction , animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.1, animations: {
self.transform = CGAffineTransform(translationX: 0, y: -50)
})
UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1, animations: {
self.transform = CGAffineTransform(translationX: 0, y: 50)
})
}) { (completed) in
print("done")
self.removeFromSuperview()
}
}
//configure internal text and color scheme
func configureToastType(){
if self.toastRenderType == "Ok" {
self.backgroundColor = UIColor(hue: 0.4222, saturation: 0.6, brightness: 0.78, alpha: 1.0)
label.text = "Configuration saved!"
dismissButton.setTitle("OK", for: .normal)
imageView.image = UIImage(named: "icon-16-check#2x.png")
}
else{
self.backgroundColor = UIColor(red: 0.87, green: 0.28, blue: 0.44, alpha: 1.00)
label.text = "Critical"
dismissButton.setTitle("Undo", for: .normal)
imageView.image = UIImage(named: "icon-16-close.png")
}
}
#objc func clickedToast(){
print("you clicked the toast button")
self.removeFromSuperview()
// delegate?.toastButtonClicked(sender)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
You have some very confusing constraints, and we're missing your config() func and ToastViewModel and an example view controller showing how you set things up...
But, after taking some guesses to fill in the missing pieces, your animation code works for me - although it animates the view up and then *immediately down.
As is obvious, animateKeyframes uses relative times. You can think of those start and duration values as percentages of the total duration.
So, if you wanted a 1-second UP animation, then pause for 8-seconds, then a 1-second DOWN animation, it would look like this:
// total duration is 10-seconds
// 1-second UP is 1/10th of total, or 0.1
// 8-second PAUSE is 8/10ths of total, or 0.8
// 1-second DOWN is 1/10th of total, or 0.1
// so the DOWN animation should start at 9-seconds
// 0.1 + 0.8 = 0.9
UIView.animateKeyframes(withDuration: 10.0, delay: 0.0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.1, animations: {
self.transform = CGAffineTransform(translationX: 0, y: -50)
})
UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1, animations: {
self.transform = CGAffineTransform(translationX: 0, y: 0)
})
}) { (completed) in
print("done")
}
Since we generally want to make code flexible, we could write it like this - very, very verbose to make things clear:
func animateUp(){
// we want both UP and DOWN animations to take 0.3 seconds each
let upAnimDuration: TimeInterval = 0.3
let downAnimDuration: TimeInterval = 0.3
// we want to pause 3-seconds while view is "up"
let pauseDuration: TimeInterval = 3.0
// so, total duration is up + pause + down
let totalDuration: TimeInterval = upAnimDuration + pauseDuration + downAnimDuration
// animateKeyframes uses *relative* times, so
// let's convert those to percentages of total
let upPCT: TimeInterval = upAnimDuration / totalDuration
let downPCT: TimeInterval = downAnimDuration / totalDuration
let pausePCT: TimeInterval = pauseDuration / totalDuration
// now let's calculate the start times
let upStart: TimeInterval = 0.0
let pauseStart: TimeInterval = upStart + upPCT
let downStart: TimeInterval = pauseStart + pausePCT
// let's transform the view UP by self's height + 20-points
let yOffset: CGFloat = self.frame.height + 20.0
// now we'll do the animation with those values
UIView.animateKeyframes(withDuration: totalDuration, delay: 0.0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: upStart, relativeDuration: upPCT, animations: {
self.transform = CGAffineTransform(translationX: 0, y: -yOffset)
})
UIView.addKeyframe(withRelativeStartTime: downStart, relativeDuration: downPCT, animations: {
self.transform = CGAffineTransform(translationX: 0, y: 0)
})
}) { (completed) in
print("done")
}
}

Get offset when Scrollview is animated

I have a scorll view with animating
UIView.animate(withDuration: Double(totalMidiTime) / 1000, delay: 0, options: .curveLinear) {
self.scrollView.contentOffset.x = self.scrollView.contentSize.width - self.leftMargin
} completion: { (_) in }
I want to get current postion offset when it's animating
You can get the offset by checking the bounds.origin.x value of the scroll view's presentation layer:
guard let pl = scrollView.layer.presentation() else {
return
}
print("Content Offset X:", pl.bounds.origin.x)
Here's a complete example...
We create a scroll view with 30 labels. On viewDidAppear we start a 20-second horizontal animation to the last label. Each time we tap the button, the "status label" will update with the current bounds.origin.x of the scroll view's presentation layer (which matches the contentOffset.x):
class ViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .systemGreen
return v
}()
let testButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .blue
v.setTitle("Get Offset", for: [])
v.setTitleColor(.lightGray, for: .highlighted)
return v
}()
let statusLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.numberOfLines = 0
v.textAlignment = .center
v.text = "Content Offset X\n0.00"
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
let stack = UIStackView()
stack.distribution = .fillEqually
stack.spacing = 8
stack.translatesAutoresizingMaskIntoConstraints = false
for i in 1...30 {
let v = UILabel()
v.backgroundColor = .yellow
v.text = "Label \(i)"
v.textAlignment = .center
stack.addArrangedSubview(v)
}
scrollView.addSubview(stack)
view.addSubview(scrollView)
view.addSubview(testButton)
view.addSubview(statusLabel)
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.heightAnchor.constraint(equalToConstant: 60.0),
stack.topAnchor.constraint(equalTo: cg.topAnchor, constant: 8.0),
stack.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 8.0),
stack.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -8.0),
stack.bottomAnchor.constraint(equalTo: cg.bottomAnchor),
stack.heightAnchor.constraint(equalTo: fg.heightAnchor, constant: -16.0),
testButton.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 20.0),
testButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testButton.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.6),
statusLabel.topAnchor.constraint(equalTo: testButton.bottomAnchor, constant: 20.0),
statusLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
statusLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.9),
])
testButton.addTarget(self, action: #selector(gotTap(_:)), for: .touchUpInside)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 20.0, delay: 0, options: .curveLinear) {
self.scrollView.contentOffset.x = self.scrollView.contentSize.width - self.scrollView.frame.width
} completion: { (_) in }
}
#objc func gotTap(_ sender: UIButton) -> Void {
guard let pl = scrollView.layer.presentation() else {
return
}
let x = String(format: "%0.2f", pl.bounds.origin.x)
statusLabel.text = "Content Offset X\n\(x)"
}
}

expand UIView height bottom to left and vice versa

I have a view that has a button on top and a textview on bottom as subviews, Im trying to have the button either expand and show the textview or collapse and hide it (like a show/hide)
I used the solution here and it kind of helped, except the subview (text view) was still showing and overlapping with other views so it only hid the main UIView.
here I initialized the height constraint to 25 to leave space for the button:
heightConstraint = detailView.heightAnchor.constraint(equalToConstant: 25)
the action function that'll expand/collapse the view based on the height constraint
#objc func expandViewPressed(sender: UIButton) {
if isAnimating { return }
let shouldCollapse = detailView.frame.height > 25
animateView(isCollapsed: shouldCollapse)
}
the animation function
private func animateView(isCollapsed: Bool) {
heightConstraint[enter image description here][1].isActive = isCollapsed
isAnimating = true
UIView.animate(withDuration: 1, animations: {
self.detailText.isHidden = isCollapsed
self.view.layoutIfNeeded()
}) { (_) in
self.isAnimating = false
}
}
—
expanded view
Sherbini .. please make sure you have added proper constraints to your subviews .. make sure not to add height constraint on any of your subview ..
also make sure you have added view.clipsToBounds == true
Hope it will work for you ..
There are various ways to approach this.
One method is to use two "bottom" constraints:
one from the bottom of the button to the bottom of detailView
one from the bottom of detailText to the bottom of detailView
Then set the constraint Priority of each based on whether the view should be "collapsed" or "expanded."
Here is a full implementation for you to try:
class ExpandViewController: UIViewController {
let myButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Collapse", for: [])
v.backgroundColor = .red
return v
}()
let detailText: UITextView = {
let v = UITextView()
v.translatesAutoresizingMaskIntoConstraints = false
v.text = "This is text in the text view."
return v
}()
let detailView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
v.clipsToBounds = true
return v
}()
var isAnimating: Bool = false
var collapsedConstraint: NSLayoutConstraint!
var expandedConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0)
detailView.addSubview(myButton)
detailView.addSubview(detailText)
view.addSubview(detailView)
let g = view.safeAreaLayoutGuide
// when collapsed, we want button bottom to constrain detailView bottom
collapsedConstraint = myButton.bottomAnchor.constraint(equalTo: detailView.bottomAnchor, constant: -12.0)
// when expanded, we want textView bottom to constrain detailView bottom
expandedConstraint = detailText.bottomAnchor.constraint(equalTo: detailView.bottomAnchor, constant: -12.0)
// we'll start in Expanded state
expandedConstraint.priority = .defaultHigh
collapsedConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
// constrain detailView Top / Leading / Trailing
detailView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
detailView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
detailView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// no Height or Bottom constraint for detailView
// constrain button Top / Center / Width
myButton.topAnchor.constraint(equalTo: detailView.topAnchor, constant: 12.0),
myButton.centerXAnchor.constraint(equalTo: detailView.centerXAnchor),
myButton.widthAnchor.constraint(equalToConstant: 200.0),
// constrain detailText Top / Leading / Trailing
detailText.topAnchor.constraint(equalTo: myButton.bottomAnchor, constant: 12.0),
detailText.leadingAnchor.constraint(equalTo: detailView.leadingAnchor, constant: 12.0),
detailText.trailingAnchor.constraint(equalTo: detailView.trailingAnchor, constant: -12.0),
// constrain detailText's Height
detailText.heightAnchor.constraint(equalToConstant: 200.0),
expandedConstraint,
collapsedConstraint,
])
myButton.addTarget(self, action: #selector(self.expandViewPressed(sender:)), for: .touchUpInside)
}
#objc func expandViewPressed(sender: UIButton) {
if isAnimating { return }
animateView()
}
private func animateView() {
isAnimating = true
// if it's expanded
if expandedConstraint.priority == .defaultHigh {
expandedConstraint.priority = .defaultLow
collapsedConstraint.priority = .defaultHigh
} else {
collapsedConstraint.priority = .defaultLow
expandedConstraint.priority = .defaultHigh
detailText.isHidden = false
}
UIView.animate(withDuration: 1, animations: {
self.view.layoutIfNeeded()
}) { (_) in
self.detailText.isHidden = self.expandedConstraint.priority == .defaultLow
self.isAnimating = false
self.myButton.setTitle(self.detailText.isHidden ? "Expand" : "Collapse", for: [])
}
}
}

Resources