Updating multiple UI Elements inside Timer.scheduledTimer doesn't work - ios

I am trying to make an image "move down" every second while also showing a time counter. I have a scheduledTimer where I change the image's Y center and update the timer. The timer label updates but the image does not move. Strangely I can get the image to move down if I comment the line where I update the timer's UILabel. So apparently I can't update both.
I have tried adding the Timer to RunLoop, using a DispatchQueue and creating a Timer that takes a selector but nothing works.
class ViewController: UIViewController {
let starImage = UIImageView(image: UIImage(systemName: "star.fill"))
let timerLabel = UILabel()
var counter = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
setupViews()
gameEngine()
}
private func setupViews() {
view.addSubview(starImage)
starImage.tintColor = .systemYellow
starImage.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(timerLabel)
timerLabel.tintColor = .white
timerLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
starImage.heightAnchor.constraint(equalToConstant: 100),
starImage.widthAnchor.constraint(equalToConstant: 100),
starImage.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
starImage.centerXAnchor.constraint(equalTo: view.centerXAnchor),
timerLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20),
timerLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
])
}
// PROBLEM HERE
private func gameEngine() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
[weak self] _ in
self?.counter += 1
self?.timerLabel.text = "Timer: \(self!.counter)"
self?.starImage.center.y += 10
}
}
}
I got it to work by using UIView.animate as Rob suggested it in the comments.
I replaced the line where I updated the Y value with this:
UIView.animate(withDuration: 1, animations: {
[weak self] in
self?.starImage.transform = CGAffineTransform(translationX: 0, y: CGFloat(self!.counter*100))
})

There are 2 issues:
Instead
timerLabel.tintColor = .white you need to use timerLabel.textColor = .white
2.To move starImage you need update constraint:
class ViewController: UIViewController {
let starImage = UIImageView(image: UIImage(systemName: "star.fill"))
let timerLabel = UILabel()
var counter = 0
var topConstraint: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
setupViews()
gameEngine()
}
private func setupViews() {
view.addSubview(starImage)
starImage.tintColor = .systemYellow
starImage.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(timerLabel)
timerLabel.tintColor = .white
timerLabel.textColor = .white
timerLabel.translatesAutoresizingMaskIntoConstraints = false
topConstraint = starImage.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
NSLayoutConstraint.activate([
starImage.heightAnchor.constraint(equalToConstant: 100),
starImage.widthAnchor.constraint(equalToConstant: 100),
topConstraint!,
starImage.centerXAnchor.constraint(equalTo: view.centerXAnchor),
timerLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20),
timerLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
])
}
// PROBLEM HERE
private func gameEngine() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
[weak self] _ in
self?.counter += 1
self?.timerLabel.text = "Timer: \(self!.counter)"
self!.topConstraint?.constant += 10
}
}
}

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

How to add a button a UIView()

So I have
let verticalViewOne = UIView()
I want to add the following button to this view
/* Station */
DRN1Button.tintColor = .systemPink
DRN1Button.frame.size = CGSize(width: 200.0, height: 200.0)
DRN1Button.setImage(UIImage(named: "DRN1.jpg"), for: UIControl.State(rawValue: UIControl.State.RawValue(play)))
//playButton.imageView?.contentMode = .scaleAspectFit
DRN1Button.contentVerticalAlignment = .fill
DRN1Button.contentHorizontalAlignment = .fill
DRN1Button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
DRN1Button.addTarget(self, action: #selector(playdrn1), for: .touchUpInside)
DRN1Button.translatesAutoresizingMaskIntoConstraints = false
// self.view.addSubview(playButton)
verticalStackView.addSubview(DRN1Button)
My question is how do I go about this?
as this code
verticalStackView.addSubview(DRN1Button)
brings an error that crashes the app.
NOTE: the button is override func viewDidLoad() { while the verticalStackView is set in
func setupHierachy(){
view.addSubview(containerStackView)
view.addSubview(verticalStackView)
verticalStackView.addArrangedSubview(verticalViewOne)
//verticalStackView.addArrangedSubview(DRN1Button)
containerStackView.addArrangedSubview(verticalStackView)
}
Error am getting is
Thread 1: Exception: "Unable to activate constraint with anchors <NSLayoutYAxisAnchor:0x281f52640 \"UIButton:0x10540ba90.centerY\"> and <NSLayoutYAxisAnchor:0x281f52680 \"UIView:0x10530ea40.centerY\"> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal."
full code.
//
// ViewController.swift
// 1life.radio
//
// Created by Russell Harrower on 19/3/20.
// Copyright © 2020 Russell Harrower. All rights reserved.
//
import UIKit
import AVKit
import Network
import Kingfisher
struct Nowplayng: Decodable{
let data: [Data]
}
struct Data: Decodable{
let track: [Trackinfo]
}
struct Trackinfo: Decodable {
let title: String
let artist: String
let imageurl: String
}
class ViewController: UIViewController {
let uuid = UIDevice.current.identifierForVendor?.uuidString
let playButton = UIButton(frame: CGRect(x: 0, y:0, width: 100, height: 100))
let DRN1Button = UIButton(frame: CGRect(x: 0, y:0, width: 100, height: 48))
var play = 0;
var npinfo = "https://api.drn1.com.au/station/1lifeRadio/playing"
var currentstation = "";
var timer = Timer()
/* V VIEWS */
let containerStackView = UIStackView()
let verticalStackView = UIStackView()
let verticalViewOne = UIView()
let verticalViewTwo = UIView()
let verticalViewThree = UIView()
/* END V VIEWS */
var isPlaying = false {
didSet {
self.playButton.setImage(UIImage(systemName: self.isPlaying ? "play.circle" : "pause.circle"), for: UIControl.State(rawValue: UIControl.State.RawValue(play)))
//self.isPlaying ? MusicPlayer.shared.startBackgroundMusic() : MusicPlayer.shared.stopBackgroundMusic()
self.isPlaying ? MusicPlayer.shared.stopBackgroundMusic() : MusicPlayer.shared.startBackgroundMusic(url: currentstation)
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
setupHierachy()
setlayout()
currentstation = "http://stream.radiomedia.com.au:8015/stream?uuid=\(self.uuid ?? "")"
// Do any additional setup after loading the view.
MusicPlayer.shared.startBackgroundMusic(url: "http://stream.radiomedia.com.au:8015/stream?uuid=\(self.uuid ?? "")")
playButton.tintColor = .systemPink
playButton.frame.size = CGSize(width: 200.0, height: 200.0)
playButton.setImage(UIImage(systemName: "pause.circle"), for: UIControl.State(rawValue: UIControl.State.RawValue(play)))
//playButton.imageView?.contentMode = .scaleAspectFit
playButton.contentVerticalAlignment = .fill
playButton.contentHorizontalAlignment = .fill
playButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
playButton.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
playButton.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(playButton)
//verticalViewOne.addSubview(DRN1Button)
NSLayoutConstraint.activate([
playButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
playButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
playButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -208),
//playButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12),
//playButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12),
playButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.2),
playButton.heightAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.2),
/* DRN1Button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
DRN1Button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
DRN1Button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0),
//playButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12),
//playButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12),
DRN1Button.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.2),
DRN1Button.heightAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.2)*/
])
self.artist.textAlignment = .center
self.song.textAlignment = .center
nowplaying()
scheduledTimerWithTimeInterval()
}
func setupViews(){
verticalStackView.axis = .vertical
verticalStackView.translatesAutoresizingMaskIntoConstraints = true
verticalStackView.distribution = .fillEqually
verticalViewOne.backgroundColor = .red
containerStackView.backgroundColor = .green
containerStackView.axis = .vertical
containerStackView.spacing = 15
containerStackView.distribution = .fillEqually
}
func setupHierachy(){
view.addSubview(containerStackView)
view.addSubview(verticalStackView)
verticalStackView.addArrangedSubview(verticalViewOne)
//verticalStackView.addArrangedSubview(DRN1Button)
containerStackView.addArrangedSubview(verticalStackView)
}
func setlayout(){
containerStackView.translatesAutoresizingMaskIntoConstraints = false
containerStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
containerStackView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2).isActive = true
containerStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
containerStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
}
#objc func appDidLaunch() {
#if targetEnvironment(simulator)
/*
if you don’t set up remote controls, the AVPlayer from
AVPlayerController will immediately create default remote controls
otherwise your simulator will not show the controls.
*/
self.isPlaying = false
self.playButton.setImage(UIImage(systemName: "pause.circle"), for: UIControl.State(rawValue: UIControl.State.RawValue(play)))
MusicPlayer.shared.startBackgroundMusic(url: "http://stream.radiomedia.com.au:8015/stream?uuid=\(uuid ?? "")")
#else
// MusicPlayer.shared.setupRemoteTransportControls()
// more custom controls set up
#endif
}
#objc func playdrn1(sender:UIButton!){
currentstation = "http://stream.radiomedia.com.au:8003/stream?uuid=\(uuid ?? "")"
MusicPlayer.shared.startBackgroundMusic(url: "http://stream.radiomedia.com.au:8003/stream?uuid=\(uuid ?? "")")
npinfo = "https://api.drn1.com.au/station/DRN1/playing"
}
#objc func buttonAction(sender: UIButton!) {
print(self.isPlaying)
self.isPlaying = !self.isPlaying
}
#IBOutlet var imageurl: UIImageView!
#IBOutlet var artist: UILabel!
#IBOutlet var song: UILabel!
func scheduledTimerWithTimeInterval(){
// Scheduling timer to Call the function "updateCounting" with the interval of 1 seconds
timer = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(self.nowplaying), userInfo: nil, repeats: true)
}
#objc func nowplaying(){
let jsonURLString = npinfo
guard let feedurl = URL(string: jsonURLString) else { return }
URLSession.shared.dataTask(with: feedurl) { (data,response,err)
in
guard let data = data else { return }
do{
let nowplaying = try JSONDecoder().decode(Nowplayng.self, from: data)
//print(nowplaying.data.first?.track.first?.artist as Any)
nowplaying.data.forEach {_ in
DispatchQueue.main.async {
self.artist.text = nowplaying.data.first?.track.first?.artist
self.song.text = nowplaying.data.first?.track.first?.title
}
if var strUrl = nowplaying.data.first?.track.first?.imageurl {
strUrl = strUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
DispatchQueue.main.sync {
let processor = DownsamplingImageProcessor(size: self.imageurl.bounds.size)
|> RoundCornerImageProcessor(cornerRadius: 20)
self.imageurl.kf.indicatorType = .activity
self.imageurl.kf.setImage(
with: URL(string: strUrl),
placeholder: UIImage(named: "placeholderImage"),
options: [
.processor(processor),
.scaleFactor(UIScreen.main.scale),
.transition(.fade(1)),
.cacheSerializer(FormatIndicatedCacheSerializer.png),
.cacheOriginalImage
])
{
result in
switch result {
case .success(let value):
print("Task done for: \(value.source.url?.absoluteString ?? "")")
case .failure(let error):
print("Job failed: \(error.localizedDescription)")
}
}
}
// self.imageurl.kf.setImage(with: URL(string: strUrl), placeholder: nil)
//MusicPlayer.shared.nowplaying(artist: $0.track.artist, song: $0.track.title, cover:strUrl)
// MusicPlayer.shared.getArtBoard(artist: $0.track.artist, song: $0.track.title, cover:strUrl)
}
}
}catch let jsonErr{
print("error json ", jsonErr)
}
}.resume()
}
}
First off, the line:
verticalStackView.addSubview(DRN1Button)
crashes because you are attempting to add a subview to stackView, you're supposed to do addArrangedSubview.
For the crash:
Thread 1: Exception: "Unable to activate constraint with anchors
<NSLayoutYAxisAnchor:0x281f52640 "UIButton:0x10540ba90.centerY"> and
<NSLayoutYAxisAnchor:0x281f52680 "UIView:0x10530ea40.centerY">
because they have no common ancestor. Does the constraint or its
anchors reference items in different view hierarchies? That's
illegal."
You have it already.
DRN1Button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
Your DRN1Button has no common ancestor with your self.view. This can mean that you haven't added your DRN1Button to a view, or most probably the line above, can't be done. While your button is in a stackview but you are setting its centerYAnchor equal to your self.view.

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.

Animate constraints change UIViewController

I have "error" view, that is placed above navigation bar and hidden. When error occured, i want that view to smoothly show from top. I tried:
class AuthViewController: UIViewController {
let error: ErrorView = {
let error = ErrorView()
error.setup()
return error
}()
var topAnchor: NSLayoutConstraint!
var botAnchor: NSLayoutConstraint!
override func viewDidLoad() {
setupErrorView()
}
private func setupErrorView(){
view.addSubview(error)
botAnchor = error.bottomAnchor.constraint(equalTo: view.topAnchor)
botAnchor.isActive = true
topAnchor = error.topAnchor.constraint(equalTo: view.topAnchor, constant: CGFloat(Offsets.navigationAndStatusBarHeight))
topAnchor.isActive = false
error.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
error.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
func showError(_ text: String){
UIView.animate(withDuration: 2.0) {[weak self] in
guard let weakSelf = self else { return }
print("attempt to animate")
weakSelf.error.show(text)
weakSelf.botAnchor.isActive = false
weakSelf.topAnchor.isActive = true
weakSelf.view.setNeedsLayout()
}
}
}
class ErrorView: UIView {
private var label: UILabel = {
return LabelSL.regular()
}()
fileprivate func setup(){
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = Theme.Color.orange.value
addSubview(label)
}
fileprivate func show(_ text: String){
let sideOffset: CGFloat = 10
let verticalOffset: CGFloat = 10
label.text = text
label.topAnchor.constraint(equalTo: topAnchor, constant: verticalOffset).isActive = true
label.leftAnchor.constraint(equalTo: leftAnchor, constant: sideOffset).isActive = true
label.rightAnchor.constraint(equalTo: rightAnchor, constant: -sideOffset).isActive = true
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -verticalOffset).isActive = true
}
}
Animation should be done when func showError(_ text: String){ method called, but it's not. View just appear instantly.
You're trying to animate constraints in wrong way. You should set constraints outside of animation block and only layoutIfNeeded in animation:
func showError(_ text: String){
botAnchor.isActive = false
topAnchor.isActive = true
error.show(text)
UIView.animate(withDuration: 2.0) {
self.view.layoutIfNeeded()
}
}

Swift: UIStackView of UIControls with selector method that doesn't fire

Introduction
I'm creating an app which uses a custom view in which I have a UIStackView to sort out 5 UIControls. When a user taps one of the UIControls an underscore line gets animated, sliding under the tapped UIControl.
However, for some reason the method/selector for these UIControls no longer gets called. I believe this has to do with that I updated my Mac to the macOS (and Xcode) update released this week (wk.44). (updated from swift 4.2 to swift 4.2.1). Before the updated this animation and selector worked perfectly. But I'm not sure. And I'm now completely stuck on what I'm doing wrong.
Context
I created a playground and scaled down everything as much as I could and the issue persists.
I have tried to define the UIStackView in the global scope of my SetupView class but it doesn't change anything. So I believe it is not an issue of the stackView or its subviews being deallocated?
Below I've provided my UIControl subclass and my SetupView (UIView subclass) that I use. I've created a playground so you may copy paste in Xcode playground to test if you want.
Question
Why doesn't the method goalViewControlTapped(_ sender: SetupViewControl) get called?
Code
import UIKit
import PlaygroundSupport
class SetupViewControl: UIControl {
let titleLabel : UILabel = {
let lbl = UILabel()
lbl.font = UIFont(name: "Futura", size: 14)
lbl.textColor = .white
lbl.backgroundColor = .clear
lbl.textAlignment = .center
lbl.translatesAutoresizingMaskIntoConstraints = false
return lbl
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupLabel()
layer.cornerRadius = 5
}
fileprivate func setupLabel() {
addSubview(titleLabel)
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5).isActive = true
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5).isActive = true
titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var isHighlighted: Bool {
didSet {
UIView.animate(withDuration: 0.12) {
self.backgroundColor = self.isHighlighted ? UIColor.lightGray : UIColor.clear
}
}
}
}
class SetupView: UIView {
let dataModel : [String] = ["2 weeks", "1 month", "2 months", "6 months", "1 year"]
var selectionLineCenterX : NSLayoutConstraint!
let selectionLine = UIView()
let labelZero = SetupViewControl()
let labelOne = SetupViewControl()
let labelTwo = SetupViewControl()
let labelThree = SetupViewControl()
let labelFour = SetupViewControl()
let labelFive = SetupViewControl()
lazy var controlArray = [self.labelZero, self.labelOne, self.labelTwo, self.labelThree, self.labelFour, self.labelFive]
init(frame: CGRect, color: UIColor) {
super.init(frame: frame)
self.backgroundColor = color
setupView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func setupView() {
layer.cornerRadius = 0
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 1
setupLabelText()
setupControlsInStackView()
}
fileprivate func setupLabelText() {
for num in 0...(dataModel.count - 1) {
controlArray[num].titleLabel.text = dataModel[num]
}
}
// let stackView = UIStackView(frame: .zero) I have tried to declare the stackView here but it doesn't fix my issue.
func setupControlsInStackView() {
var stackViewArray = [SetupViewControl]()
for num in 0...(dataModel.count - 1) {
controlArray[num].isUserInteractionEnabled = true
controlArray[num].addTarget(self, action: #selector(goalViewControlTapped(_:)), for: .touchUpInside)
stackViewArray.append(controlArray[num])
}
let stackView = UIStackView(arrangedSubviews: stackViewArray)
stackView.alignment = .fill
stackView.distribution = .fillEqually
stackView.axis = .horizontal
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8).isActive = true
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8).isActive = true
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 15).isActive = true
addSubview(selectionLine)
selectionLine.backgroundColor = .white
selectionLine.translatesAutoresizingMaskIntoConstraints = false
selectionLine.heightAnchor.constraint(equalToConstant: 1).isActive = true
selectionLine.topAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
selectionLine.widthAnchor.constraint(equalToConstant: 50).isActive = true
selectionLineCenterX = selectionLine.centerXAnchor.constraint(equalTo: leadingAnchor, constant: -100)
selectionLineCenterX.isActive = true
}
#objc fileprivate func goalViewControlTapped(_ sender: SetupViewControl) {
print("This is not getting printed!!!")
selectionLineCenterX.isActive = false
selectionLineCenterX = selectionLine.centerXAnchor.constraint(equalTo: sender.centerXAnchor)
selectionLineCenterX.isActive = true
UIView.animate(withDuration: 0.25, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseIn, animations: {
self.layoutIfNeeded()
}, completion: nil)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let testView = SetupView(frame: .zero, color: UIColor.blue)
view.addSubview(testView)
testView.translatesAutoresizingMaskIntoConstraints = false
testView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
testView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
testView.heightAnchor.constraint(equalToConstant: 100).isActive = true
testView.widthAnchor.constraint(equalToConstant: 365).isActive = true
}
}
// For live view in playground
let vc = ViewController()
vc.preferredContentSize = CGSize(width: 375, height: 812)
PlaygroundPage.current.liveView = vc
Thanks for reading my question.
Does your UIStackView show as having an ambiguous layout when you open the view debugger? If so, that may be causing the internal views to not receive the touch events.
You can provide UIStackView with either:
x and y constraints only
or
x, y, width and height.
In the above case the height constraint is missing.

Resources