Animate UIView transform happens instantly instead of over duration value - ios

I am trying to animate a view on screen in a similar way to how action sheets appear.
My router presents CustomCardViewController which has an overlay background.
After a short delay I'd like containerView too animate into view from the bottom.
Instead what is happening however is it just appears in place. There is no animation between the transition.
final class CustomCardViewController: UIViewController {
private let backgroundMask: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .init(white: 0, alpha: 0.3)
return view
}()
private lazy var containerView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
view.transform = .init(translationX: 0, y: view.frame.height)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
backgroundMask.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTapToDismiss)))
modalPresentationStyle = .overFullScreen
[backgroundMask, containerView].forEach(view.addSubview(_:))
NSLayoutConstraint.activate([
backgroundMask.topAnchor.constraint(equalTo: view.topAnchor),
backgroundMask.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backgroundMask.bottomAnchor.constraint(equalTo: view.bottomAnchor),
backgroundMask.trailingAnchor.constraint(equalTo: view.trailingAnchor),
containerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 24),
containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -24),
containerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -24),
containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 200)
])
UIView.animate(withDuration: 5, delay: 0.33, options: .curveEaseOut, animations: {
self.containerView.transform = .identity
}, completion: nil)
}
}
private extension CustomCardViewController {
#objc func onTapToDismiss() {
dismiss(animated: false, completion: nil)
}
}

You need to call your animation block from another lifecycle method. You can't trigger this from viewDidLoad as the view has only loaded, there is nothing on screen yet.
Try using viewDidAppear

For animation, you need to update the constant of the constrain that you want to animate. Here, since you're trying to animate from the bottom, you need to update a vertical constrain in this case, the bottom constraint. Here's the code for animation:
final class CustomCardViewController: UIViewController {
//..
override func viewDidLoad() {
super.viewDidLoad()
NSLayoutConstraint.activate([
// remove bottom constraint from here
])
containerViewBottomConstraint = containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 250)
containerViewBottomConstraint?.isActive = true
}
var containerViewBottomConstraint: NSLayoutConstraint? // declare bottom constraint
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
containerViewBottomConstraint?.constant = -24
UIView.animate(withDuration: 5, delay: 0.33, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
})
}
}

Related

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

Transparency on a UITableView causes navigation animation to look bad

I'm using a UITableView with a Navigation Controller and I have made the former partially transparent, which looks great.
The problem I am running into is that when I press a button, the transition animation (to change to another view) looks odd because the old view that is sliding behind the new one is visible for a time.
I have tried things like temporarily shutting off transparency (either suddenly, or gradually), and while it looks a little better, overall the experience still isn't great.
I guess it might be possible to do a custom animation, but this seems like a bad idea since it will likely look different than the built-in OS animation. Actually, even with a custom animation I am not sure how I would do it since I think I would run into the same issue.
Does anyone have any ideas how I can make things look cleaner?
UPDATE: adding more detail based on questions asked in the comments
The UI is a pretty complex set of pieces but I'll try to describe the relevant parts here.
There is a UISplitViewController [A], and I have created a UIVisualEffectView (with UIBlurEffect) that is attached as a subview of A's parent. My menu consists of a UINavigationController [B], and a UITableViewController [C] that is the top level of the menu. [B] is added as a subview as the content view of the blur effect view.
Two other UITableViewControllers [D] and [E] are transitioned to when button [1] or [2] are pressed on [C].
There are a few other view controllers that are subviews of [A] (or [A]'s parent) that are showing through, blurred, but that is the design and there is no issue there.
The problem is for the transition animations from [C]->[D], [D]->[C] (via back button), [C]->[E], or [E]->[C], you can see the controller that is moving away behind the controller that is coming in. So if you do [C]->[D] (via pressing button [1] on [C]) then you will see [C] going behind [D] as it slides in, and [C] eventually disappears.
The actual showing of [D] or [E] is done via a line of code like this (inside the custom class of [C])
self.navigationController?.show(myVC, sender: self)
where navigationController is [B] and myVC is [D].
The transition back to [C] is done via popViewController().
OK - trying to (minimally) emulate your setup description...
View controller with an image view filling the entire view
Navigation controller added as a child VC
Two VCs for the nav controller...
both with transparent background
"Page 1" pushes to "Page 2"
So I assume you mean you have a current "push/pop" transition that looks like this with simulator Debug -> Slow Animations to exaggerate the effect (these are kinda "heavy" gifs, so open them in a new browser tab if the animation isn't running):
And your goal is something close or similar to this:
You will likely need to use a custom transition.
I was able to get those results using the code from this article unedited: Simple, custom navigation transitions -- note: this is not mine - just found it from quick searching.
Here's the code for the full example -- everything is done via code, no #IBOutlet or #IBAction connections needed. Just assign a new view controller's custom class as NavSubVC :
class NavSubVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
guard let img = UIImage(named: "navBKG") else {
DispatchQueue.main.async {
let a = UIAlertController(title: "Alert", message: "Could not load \"navBKG\" image", preferredStyle: .alert)
self.present(a, animated: true, completion: nil)
}
return
}
let imgView = UIImageView(image: img)
imgView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imgView)
let rvc = Page1VC()
let navC = UINavigationController(rootViewController: rvc)
self.addChild(navC)
guard let navView = navC.view else { return }
view.addSubview(navView)
navC.didMove(toParent: self)
navView.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
imgView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
navView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
navView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
navView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
navView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
// let's have a gray nav bar always showing
let navigationBarAppearance = UINavigationBarAppearance()
navigationBarAppearance.configureWithTransparentBackground()
navigationBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
navigationBarAppearance.backgroundColor = .systemGray
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
UINavigationBar.appearance().compactAppearance = navigationBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
// let's add a border to the navigation controller view
// so we can see its frame (since the controllers have clear backgrounds)
navView.layer.borderWidth = 2
navView.layer.borderColor = UIColor.yellow.cgColor
// un-comment this line to see the custom transition
//navC.addCustomTransitioning()
}
}
class PageBaseVC: UIViewController {
var labels: [UILabel] = []
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
for i in 1...6 {
let v = UILabel()
v.text = "\(i)"
v.textAlignment = .center
v.textColor = .white
v.translatesAutoresizingMaskIntoConstraints = false
v.widthAnchor.constraint(equalToConstant: 80.0).isActive = true
v.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
labels.append(v)
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
labels[0].topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
labels[0].leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
labels[1].centerYAnchor.constraint(equalTo: g.centerYAnchor, constant: 0.0),
labels[1].leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
labels[2].bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
labels[2].leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
labels[3].topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
labels[3].trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
labels[4].centerYAnchor.constraint(equalTo: g.centerYAnchor, constant: 0.0),
labels[4].trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
labels[5].bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
labels[5].trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
])
}
}
class Page1VC: PageBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Page 1"
labels.forEach { v in
v.backgroundColor = .systemBlue
}
let b = UIButton()
b.backgroundColor = .systemGreen
b.setTitle("Push to Page 2", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(b)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
b.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
b.centerXAnchor.constraint(equalTo: g.centerXAnchor),
b.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.75),
b.heightAnchor.constraint(equalToConstant: 60.0),
])
b.addTarget(self, action: #selector(doPush(_:)), for: .touchUpInside)
}
#objc func doPush(_ sender: Any?) {
let vc = Page2VC()
self.navigationController?.pushViewController(vc, animated: true)
}
}
class Page2VC: PageBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Page 2"
labels.forEach { v in
v.backgroundColor = .systemRed
}
}
}
// Custom Navigation Transition
// from: https://ordinarycoding.com/articles/simple-custom-uinavigationcontroller-transitions/
final class TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
// 1
let presenting: Bool
// 2
init(presenting: Bool) {
self.presenting = presenting
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
// 3
return TimeInterval(UINavigationController.hideShowBarDuration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// 4
guard let fromView = transitionContext.view(forKey: .from) else { return }
guard let toView = transitionContext.view(forKey: .to) else { return }
// 5
let duration = transitionDuration(using: transitionContext)
// 6
let container = transitionContext.containerView
if presenting {
container.addSubview(toView)
} else {
container.insertSubview(toView, belowSubview: fromView)
}
// 7
let toViewFrame = toView.frame
toView.frame = CGRect(x: presenting ? toView.frame.width : -toView.frame.width, y: toView.frame.origin.y, width: toView.frame.width, height: toView.frame.height)
let animations = {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.5) {
toView.alpha = 1
if self.presenting {
fromView.alpha = 0
}
}
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1) {
toView.frame = toViewFrame
fromView.frame = CGRect(x: self.presenting ? -fromView.frame.width : fromView.frame.width, y: fromView.frame.origin.y, width: fromView.frame.width, height: fromView.frame.height)
if !self.presenting {
fromView.alpha = 0
}
}
}
UIView.animateKeyframes(withDuration: duration,
delay: 0,
options: .calculationModeCubic,
animations: animations,
completion: { finished in
// 8
container.addSubview(toView)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
final class TransitionCoordinator: NSObject, UINavigationControllerDelegate {
// 1
var interactionController: UIPercentDrivenInteractiveTransition?
// 2
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .push:
return TransitionAnimator(presenting: true)
case .pop:
return TransitionAnimator(presenting: false)
default:
return nil
}
}
// 3
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
}
extension UINavigationController {
// 1
static private var coordinatorHelperKey = "UINavigationController.TransitionCoordinatorHelper"
// 2
var transitionCoordinatorHelper: TransitionCoordinator? {
return objc_getAssociatedObject(self, &UINavigationController.coordinatorHelperKey) as? TransitionCoordinator
}
func addCustomTransitioning() {
// 3
var object = objc_getAssociatedObject(self, &UINavigationController.coordinatorHelperKey)
guard object == nil else {
return
}
object = TransitionCoordinator()
let nonatomic = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
objc_setAssociatedObject(self, &UINavigationController.coordinatorHelperKey, object, nonatomic)
// 4
delegate = object as? TransitionCoordinator
// 5
let edgeSwipeGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
edgeSwipeGestureRecognizer.edges = .left
view.addGestureRecognizer(edgeSwipeGestureRecognizer)
}
// 6
#objc func handleSwipe(_ gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
guard let gestureRecognizerView = gestureRecognizer.view else {
transitionCoordinatorHelper?.interactionController = nil
return
}
let percent = gestureRecognizer.translation(in: gestureRecognizerView).x / gestureRecognizerView.bounds.size.width
if gestureRecognizer.state == .began {
transitionCoordinatorHelper?.interactionController = UIPercentDrivenInteractiveTransition()
popViewController(animated: true)
} else if gestureRecognizer.state == .changed {
transitionCoordinatorHelper?.interactionController?.update(percent)
} else if gestureRecognizer.state == .ended {
if percent > 0.5 && gestureRecognizer.state != .cancelled {
transitionCoordinatorHelper?.interactionController?.finish()
} else {
transitionCoordinatorHelper?.interactionController?.cancel()
}
transitionCoordinatorHelper?.interactionController = nil
}
}
}

UIView animating its own height

I have made a simple example of a UIView inside a UIViewController, so that when I tap on the inner view, it increases and decreases its height. That works fine, but I can't make the height transition animate.
Here is the view controller, followed by the view:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let theView = MyView()
theView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(theView)
NSLayoutConstraint.activate([
theView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 50.0),
theView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant:0.0),
theView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: 0.0),
theView.heightAnchor.constraint(equalToConstant: 925)
])
}
}
class MyView: UIView {
fileprivate var closedChanger = NSLayoutConstraint()
fileprivate var isOpen = false
init() {
super.init(frame: CGRect.zero)
setupView()
}
fileprivate func setupView() {
self.backgroundColor = .gray
self.translatesAutoresizingMaskIntoConstraints = true
closedChanger = self.heightAnchor.constraint(equalToConstant: 150.0)
NSLayoutConstraint.activate([
closedChanger // start off with it shorter
])
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:))))
}
#objc func handleTap(sender: UITapGestureRecognizer) {
UIView.animate(withDuration: 1.0, animations: {
self.closedChanger.constant = self.isOpen ? self.closedChanger.constant - 25 : self.closedChanger.constant + 25
// self.layoutIfNeeded()
})
isOpen.toggle()
}
}
The layoutIfNeeded doesn't affect it. Is a UIView not allowed to animate its own height change?
Your are kind of right when you say that a view can't animate itself. The problem is that you need to animate the change to the containing view, since that the is the view hierarchy that is changing.
You need to make the change to the constraint outside of the animation block, and then call layoutIfNeeded on the superview in the animation block.
#objc func handleTap(sender: UITapGestureRecognizer) {
self.closedChanger.constant = self.isOpen ? self.closedChanger.constant - 25 : self.closedChanger.constant + 25
UIView.animate(withDuration: 1.0, animations: {
self.superview?.layoutIfNeeded()
})
isOpen.toggle()
}

Simple slide up animation on UIView using NSLayoutConstraints

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.

How to find out the distance from the bottom safe area edge to the bottom of the screen?

I need to calculate the distance between the bottom anchor of the safe area and the bottom of the screen. Is there a way to do that in code given a view?
Try this one
if #available(iOS 11.0, *) {
let window = UIApplication.shared.keyWindow
let bottomPadding = window?.safeAreaInsets.bottom
}
You could try pinning a subview (clear , hidden or whatever) to the bottom of the safeAreaLayoutGuide and calculate the difference between the bottom of this view and your view controller's view in viewDidLayoutSubviews.
class ViewController: UIViewController {
let measuringView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
measuringView.backgroundColor = .magenta
measuringView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(measuringView)
let vConstraint = measuringView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
let heightConstraint = measuringView.heightAnchor.constraint(equalToConstant: 34)
var constraints = NSLayoutConstraint.constraints(withVisualFormat: "|[measuring]|", options: [], metrics: nil, views: ["measuring": measuringView])
constraints.append(vConstraint)
constraints.append(heightConstraint)
NSLayoutConstraint.activate(constraints)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let measuringBottom = measuringView.frame.origin.y + measuringView.frame.height
let viewBottom = view.bounds.height
let distance = abs(measuringBottom - viewBottom)
print("distance is \(distance) points")
}
}
To re-iterate previous answers. Pin a subview to the bottom of the view of a UIViewController. Then pin a second one to the view.safeAreaLayoutGuide.bottomAnchor anchor of the view. With both subviews pinning the top, leading and trailing anchors of the parent view. Then, I would assume in viewDidAppear, you could print out the difference between the two subview's frame.maxY values. This should give you the difference.
let viewA = UIView()
let viewB = UIView()
override func viewDidLoad() {
view.addSubview(viewA)
view.addSubview(viewB)
viewA.translateAutoResizingMasksIntoConstraints = false
viewB.translateAutoResizingMasksIntoConstraints = false
if #available(iOS 11.0, *) {
NSLayoutConstraint.activate([viewA.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0),
viewA.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0),
viewA.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0),
viewA.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0),
viewB.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0),
viewB.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0),
viewB.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0),
viewB.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0)])
} else {
// Fallback on earlier versions
}
}
override func viewDidAppear() {
super.viewDidAppear()
print("Safe Distance Value is:\(viewA.frame.maxY - viewB.frame.maxY)")
}
As a reference for others, the value appears to be 34 on an iPhone X simulator
Here is a solution that works well on tableviews with adding a bottomView for buttons:
let buttonsView = UIView()
buttonsView.translatesAutoresizingMaskIntoConstraints = false
buttonsView.backgroundColor = .secondarySystemBackground
self.tableView.addSubview(buttonsView)
let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
let bottomPadding = window?.safeAreaInsets.bottom ?? 0
buttonsView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: bottomPadding ).isActive = true
buttonsView.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor).isActive = true
buttonsView.rightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.rightAnchor).isActive = true
buttonsView.heightAnchor.constraint(equalToConstant: 88.0 + bottomPadding ).isActive = true

Resources