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()
})
})
}
})
}
}
Related
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")
}
}
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)"
}
}
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: [])
}
}
}
I'm trying to add layoutMargins to some elements in a UIStackView I'm creating in code.
I've tried the same thing in IB and it works perfectly, but when I add layoutMargins and I set isLayoutMarginsRelativeArrangement = true then my stack view's alignment breaks.
The code of my stack view is the following:
#objc lazy var buttonsStackView: UIStackView = {
let stack = UIStackView(arrangedSubviews: [doneButton, separatorView, giftButton])
stack.layoutMargins = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4)
stack.isLayoutMarginsRelativeArrangement = true
stack.axis = .horizontal
stack.frame = CGRect(x: 0, y: 0, width: 150, height: 44)
stack.spacing = 4
stack.distribution = .equalCentering
stack.alignment = .center
let bgView = UIView(frame: stack.bounds)
stackViewBackground = bgView
bgView.backgroundColor = ColorManager.shared.grayColor
bgView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
stack.insertSubview(bgView, at: 0)
separatorView.heightAnchor.constraint(equalTo: stack.heightAnchor, multiplier: 0.8).isActive = true
return stack
}()
SeparatorView only has a 1pt width constraint, while the two buttons are left unconstrained to keep theirintrinsicContentSize.
Here's how my stackView looks when isLayoutMarginsRelativeArrangement is false:
But obviously, the left and right margins are needed, so when setting isLayoutMarginsRelativeArrangement to true, my stackview's alignment breaks:
Unfortunately, I cannot use IB for this particular view and I need to initialise it from code. Any idea on how to fix this is greatly appreciated. Thank you!
Here is an example of making that a custom view, with the separator centered horizontally, and the buttons centered in each "side":
protocol DoneGiftDelegate: class {
func doneButtonTapped()
func giftButtonTapped()
}
class DoneGiftView: UIView {
weak var delegate: DoneGiftDelegate?
let doneButton: UIButton = {
let v = UIButton(type: .system)
v.setTitle("Done", for: [])
v.tintColor = .white
return v
}()
let giftButton: UIButton = {
let v = UIButton(type: .system)
v.setImage(UIImage(systemName: "gift.fill"), for: [])
v.tintColor = .white
return v
}()
let separatorView: UIView = {
let v = UIView()
v.backgroundColor = .white
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .lightGray // ColorManager.shared.grayColor
[doneButton, separatorView, giftButton].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
// width and height constraints: 150 x 44
// set Priority to 999 so it can be overriden by controller if desired
let widthConstraint = widthAnchor.constraint(equalToConstant: 150.0)
widthConstraint.priority = UILayoutPriority(rawValue: 999)
let heightConstraint = heightAnchor.constraint(equalToConstant: 44.0)
heightConstraint.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
// doneButton Leading to Leading
doneButton.leadingAnchor.constraint(equalTo: leadingAnchor),
// separator Leading to doneButton Trailing
separatorView.leadingAnchor.constraint(equalTo: doneButton.trailingAnchor),
// giftButton Leading to separator Trailing
giftButton.leadingAnchor.constraint(equalTo: separatorView.trailingAnchor),
// giftButton Trailing to Trailing
giftButton.trailingAnchor.constraint(equalTo: trailingAnchor),
// all centered vertically
doneButton.centerYAnchor.constraint(equalTo: centerYAnchor),
separatorView.centerYAnchor.constraint(equalTo: centerYAnchor),
giftButton.centerYAnchor.constraint(equalTo: centerYAnchor),
// doneButton Height = Height
doneButton.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0),
// separator Height = 80% of Height
separatorView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.8),
// giftButton Height = Height
giftButton.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0),
// separator Width = 1
separatorView.widthAnchor.constraint(equalToConstant: 1.0),
// doneButton Width = giftButton Width
doneButton.widthAnchor.constraint(equalTo: giftButton.widthAnchor, multiplier: 1.0),
// self Width and Height
widthConstraint,
heightConstraint,
])
// add target actions for buttons
doneButton.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
giftButton.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
#objc func buttonTapped(_ sender: UIButton) -> Void {
if sender == doneButton {
delegate?.doneButtonTapped()
} else {
delegate?.giftButtonTapped()
}
}
}
class DemoViewController: UIViewController, DoneGiftDelegate {
let doneGiftView: DoneGiftView = DoneGiftView()
let testView: UIView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
doneGiftView.translatesAutoresizingMaskIntoConstraints = false
testView.translatesAutoresizingMaskIntoConstraints = false
testView.addSubview(doneGiftView)
view.addSubview(testView)
// so we can see the view frame
testView.backgroundColor = .cyan
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// testView Top: 100
// Leading / Trailing with 20-pts "padding"
// Height: 80
testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
testView.heightAnchor.constraint(equalToConstant: 80.0),
// doneGiftView Trailing to testView Trailing
doneGiftView.trailingAnchor.constraint(equalTo: testView.trailingAnchor),
// doneGiftView centered vertically in testView
doneGiftView.centerYAnchor.constraint(equalTo: testView.centerYAnchor),
])
doneGiftView.delegate = self
}
func doneButtonTapped() {
print("Done button tapped!")
// do what we want when Done button tapped
}
func giftButtonTapped() {
print("Gift button tapped")
// do what we want when Gift button tapped
}
}
Example result:
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.