Why does my UIView.Transition not work unless wrapped in a UIView.Animate block? - ios

I am using a UIView.transition to flip a card over.
While troubleshooting it not working, I stumbled upon a way to make it work - but I have no idea why. I am hoping that someone can look at the two code blocks below and help me understand why one works and the other doesn't. It seems very strange to me.
First, here is the code block that actually works. The card flip is visually flawless.
UIView.animate(withDuration: 0.01) {
imageView.alpha = 1.0
imageView.layoutIfNeeded() // Works with and without this layoutIfNeeded()
} completion: { (true) in
UIView.transition(with: imageView, duration: 1.2, options: animation) {
imageView.image = endingImage
imageView.layoutIfNeeded() // Works with and without this layoutIfNeeded()
} completion: { (true) in
if self.dealTicketState.isTicketFaceUp == true { self.faceDownView.alpha = 0.0 } else { self.faceDownView.alpha = 1.0 }
UIView.animate(withDuration: 0.01) {
self.coveringLabel.backgroundColor = .clear
self.coveringLabel.layoutIfNeeded()
imageView.removeFromSuperview()
self.tickNumLabel.alpha = originalTicketNumAlpha
}
}
}
But I don't understand why I seem to need to wrap the UIView.transition() into the completion handler of a call to UIView.animate() in order for the flip animation to work.
*(Note: If I pull the "imageView.alpha = 1.0" out of the animate() block and place it immediately BEFORE calling UIView.animate() - the flip animation does not occur (with or without the layoutIfNeeded() call). It just toggles the images. *
Now, here is the code that I expected to work - but when I use this code instead of the above code, there is no "flip" transition. The card image just immediately toggles between the face up and face down image. The "UIView.transition" call here is identical to the one in the above code. The only difference here is that it's NOT wrapped into a 0.01 second UIView.animate completion block.
imageView.alpha = 1.0
imageView.layoutIfNeeded()
UIView.transition(with: imageView, duration: 1.2, options: animation) {
imageView.image = endingImage
imageView.layoutIfNeeded() // same behaviour with and without this line
} completion: { (true) in
if self.dealTicketState.isTicketFaceUp == true { self.faceDownView.alpha = 0.0 } else { self.faceDownView.alpha = 1.0 }
UIView.animate(withDuration: 0.01) {
self.coveringLabel.backgroundColor = .clear
self.coveringLabel.layoutIfNeeded()
imageView.removeFromSuperview()
self.tickNumLabel.alpha = originalTicketNumAlpha
}
}
This transition ends my flipTicket() function. The code that precedes this transition is identical in both cases. I wasn't going to include it because I don't think it's necessary to understand the problem - but then again - what do I know? Here's the stuff that came before the above clips:
func flipTicket() {
let originalTicketNumAlpha = self.tickNumLabel.alpha
self.tickNumLabel.alpha = 0.0
let tempFaceDownImage:UIImage = self.dealTicketState.faceDownImage
let tempFaceUpImage:UIImage = getCurrentFaceUpImage()
var endingImage:UIImage = self.dealTicketState.faceDownImage
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleToFill
imageView.clipsToBounds = true
imageView.alpha = 0.0
self.coveringLabel.alpha = 1.0
self.coveringLabel.backgroundColor = .black
self.coveringLabel.layoutIfNeeded()
var animation:UIView.AnimationOptions = .transitionFlipFromLeft
if faceDownView.alpha == 1.0 {
animation = .transitionFlipFromRight
imageView.image = tempFaceDownImage
endingImage = tempFaceUpImage
} else {
animation = .transitionFlipFromLeft
imageView.image = tempFaceUpImage
}
self.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: self.topAnchor),
imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
])
imageView.layoutIfNeeded()
Background:
This code is part of a custom UI control which represents a playing card. It consists of several subviews. The topmost subview was originally a UIImageView which held the image of the back of the card. This let me simply toggle the alpha for that top view to display the card as either face up or face down. And then I added one more topMost view to the control - a UILabel with "background = .clear" and attached a UITapGestureRecognizer to it. When the control is tapped, this function which is meant to animate the flipping over of the card is called.
To construct the animation, I call the getCurrentFaceUp() function which temporarily sets the alpha of the card's faceDownView to 0 (so I can take a snapshot of the card underneath it as it is currently configured). It returns a UIImage of the "face up" view of the card. I already have a UIImage of the faceDown view. These are the 2 images I need for the transition.
So...then I set the background color of that topMost UILabel to .black, create a new temporary UIImageView and place it on top of the existing control. I set the temporary imageView to initially display whichever one of the 2 images is currently visible on the control. And then I run the flip transition, change the configuration of the background control to match the new state, change the label background back to .clear and dispose of the temporary UIImageView.
(If there's a better way to accomplish this, I'm open to hearing it but the main purpose of this post is to understand why my code appears to be acting strangely.)
When I was looking for a way to animate a card flip, I found a YouTube video that demonstrated the UIView.transition() with the flip animation. It did not require using a UIView.animate() wrapper in order to make it work - so I'm pretty certain that it's me that did something wrong -but I've spent hours testing variations and searching for someone else with this problem and I haven't been able to find the answer.
Thanks very much in advance to anybody that can help me understand what's going on here...

A little tough to tell (didn't try to actually run your code), but I think you may be doing more than you need to.
Take a look at this...
We'll start with two "Card View" subclasses - Front and Back (we'll "style" them in the next step):
class CardFrontView: UIView {
}
class CardBackView: UIView {
}
Then, a "Playing Card View" class, that contains a "Front" view (cyan) and a "Back" view (red). On init, we add the subviews and set the "Front" view hidden. On tap, we'll run the flip transition between the Front and Back views:
class PlayingCardView: UIView {
let cardFront: CardFrontView = CardFrontView()
let cardBack: CardBackView = CardBackView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add both card views
// constraining all 4 sides to self
[cardFront, cardBack].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: topAnchor),
v.leadingAnchor.constraint(equalTo: leadingAnchor),
v.trailingAnchor.constraint(equalTo: trailingAnchor),
v.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
cardFront.backgroundColor = .cyan
cardBack.backgroundColor = .red
// start with cardFront hidden
cardFront.isHidden = true
// add a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(flipMe))
addGestureRecognizer(t)
}
#objc func flipMe() -> Void {
// fromView is the one that is NOT hidden
let fromView = cardBack.isHidden ? cardFront : cardBack
// toView is the one that IS hidden
let toView = cardBack.isHidden ? cardBack : cardFront
// if we're going from back-to-front
// flip from left
// else
// flip from right
let direction: UIView.AnimationOptions = cardBack.isHidden ? .transitionFlipFromRight : .transitionFlipFromLeft
UIView.transition(from: fromView,
to: toView,
duration: 0.5,
options: [direction, .showHideTransitionViews],
completion: { b in
// if we want to do something on completion
})
}
}
and then here's a simple Controller example:
class FlipCardVC: UIViewController {
let pCard: PlayingCardView = PlayingCardView()
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
pCard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pCard)
NSLayoutConstraint.activate([
pCard.centerXAnchor.constraint(equalTo: g.centerXAnchor),
pCard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
pCard.widthAnchor.constraint(equalToConstant: 200.0),
pCard.heightAnchor.constraint(equalTo: pCard.widthAnchor, multiplier: 1.5),
])
}
}
The result:
So, next step, we'll add a little styling to the Front and Back views -- no changes to the PlayingCardView functionality... just a couple new lines to set the styling...
Card Front View - with rounded corners, a border and labels at the corners and center:
class CardFrontView: UIView {
var theLabels: [UILabel] = []
var cardID: Int = 0 {
didSet {
theLabels.forEach {
$0.text = "\(cardID)"
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
for i in 1...5 {
let v = UILabel()
v.font = .systemFont(ofSize: 24.0)
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
switch i {
case 1:
v.topAnchor.constraint(equalTo: topAnchor, constant: 10.0).isActive = true
v.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
case 2:
v.topAnchor.constraint(equalTo: topAnchor, constant: 10.0).isActive = true
v.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0).isActive = true
case 3:
v.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10.0).isActive = true
v.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
case 4:
v.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10.0).isActive = true
v.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0).isActive = true
default:
v.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
v.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
theLabels.append(v)
}
layer.cornerRadius = 6
// border
layer.borderWidth = 1.0
layer.borderColor = UIColor.gray.cgColor
}
}
Looks like this:
Card Back View - with rounded corners, a border and a cross-hatch pattern:
class CardBackView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.cornerRadius = 6
// border
layer.borderWidth = 1.0
layer.borderColor = UIColor.gray.cgColor
layer.masksToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
// simple cross-hatch pattern
let hReplicatorLayer = CAReplicatorLayer()
let vReplicatorLayer = CAReplicatorLayer()
let line = CAShapeLayer()
let pth = UIBezierPath()
pth.move(to: CGPoint(x: 0.0, y: 0.0))
pth.addLine(to: CGPoint(x: 20.0, y: 20.0))
pth.move(to: CGPoint(x: 20.0, y: 0.0))
pth.addLine(to: CGPoint(x: 0.0, y: 20.0))
line.strokeColor = UIColor.yellow.cgColor
line.lineWidth = 1
line.path = pth.cgPath
var instanceCount = Int((bounds.maxX + 0.0) / 20.0)
hReplicatorLayer.instanceCount = instanceCount
hReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(20, 0, 0)
instanceCount = Int((bounds.maxY + 0.0) / 20.0)
vReplicatorLayer.instanceCount = instanceCount
vReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(0, 20, 0)
hReplicatorLayer.addSublayer(line)
vReplicatorLayer.addSublayer(hReplicatorLayer)
layer.addSublayer(vReplicatorLayer)
}
}
Looks like this:
Playing Card View - only change is setting the Front Card background color to white, and setting its "ID" to 5:
class PlayingCardView: UIView {
let cardFront: CardFrontView = CardFrontView()
let cardBack: CardBackView = CardBackView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add both card views
// constraining all 4 sides to self
[cardFront, cardBack].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: topAnchor),
v.leadingAnchor.constraint(equalTo: leadingAnchor),
v.trailingAnchor.constraint(equalTo: trailingAnchor),
v.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
cardFront.backgroundColor = .white
cardFront.cardID = 5
cardBack.backgroundColor = .red
// start with cardFront hidden
cardFront.isHidden = true
// add a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(flipMe))
addGestureRecognizer(t)
}
#objc func flipMe() -> Void {
// fromView is the one that is NOT hidden
let fromView = cardBack.isHidden ? cardFront : cardBack
// toView is the one that IS hidden
let toView = cardBack.isHidden ? cardBack : cardFront
// if we're going from back-to-front
// flip from left
// else
// flip from right
let direction: UIView.AnimationOptions = cardBack.isHidden ? .transitionFlipFromRight : .transitionFlipFromLeft
UIView.transition(from: fromView,
to: toView,
duration: 0.5,
options: [direction, .showHideTransitionViews],
completion: { b in
// if we want to do something on completion
})
}
}
and finally, the same Controller example:
class FlipCardVC: UIViewController {
let pCard: PlayingCardView = PlayingCardView()
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
pCard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pCard)
NSLayoutConstraint.activate([
pCard.centerXAnchor.constraint(equalTo: g.centerXAnchor),
pCard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
pCard.widthAnchor.constraint(equalToConstant: 200.0),
pCard.heightAnchor.constraint(equalTo: pCard.widthAnchor, multiplier: 1.5),
])
}
}
and here's the new result:

Related

Swift - animate linear diagram with minimal segment

I have a linear diagram view which basically is nothing but three views. They represent the percentage of certain asset in a portfolio. So, when I click a button, I call update(multipliers: [CGFloat]) and the animation starts. However, I ran into a few issues. Imagine if I have $10000 in stocks, $5000 in bonds and $1 in ETFs. The ETF view won't be visible at all because it's so small. But even in this case I want to show a 10px or something width view. How can I achieve this? Also, my debugger keeps warning me
Unable to simultaneously satisfy constraints. Probably at least one
of the constraints in the following list is one you don't want.
which is understandable, but I don't think is fixable because of multipliers and constants not matching the width.
So, how can I solve the issue?
import UIKit
class StripView: UIView {
var stocksConstraint = NSLayoutConstraint()
var bondsConstraint = NSLayoutConstraint()
var etfConstraint = NSLayoutConstraint()
let stocksView: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.layer.cornerRadius = 4
return view
}()
let bondsView: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.layer.cornerRadius = 4
return view
}()
let etfView: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
view.layer.cornerRadius = 4
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
layer.cornerRadius = 4
layer.masksToBounds = true
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(multipliers: [CGFloat]) {
stocksView.removeConstraint(stocksConstraint)
stocksConstraint = stocksView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: multipliers[0])
stocksConstraint.isActive = true
bondsView.removeConstraint(bondsConstraint)
bondsConstraint = bondsView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: multipliers[1])
bondsConstraint.isActive = true
etfView.removeConstraint(etfConstraint)
etfConstraint = etfView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: multipliers[2])
etfConstraint.isActive = true
UIView.animate(withDuration: 1.0) {
self.layoutIfNeeded()
self.stocksView.backgroundColor = .systemRed
self.bondsView.backgroundColor = .systemGreen
self.etfView.backgroundColor = .systemBlue
}
}
func setupViews() {
let views = [stocksView, bondsView, etfView]
views.forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
stocksConstraint = stocksView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.33)
bondsConstraint = bondsView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.33)
etfConstraint = etfView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.33)
NSLayoutConstraint.activate([
stocksView.topAnchor.constraint(equalTo: topAnchor),
stocksView.bottomAnchor.constraint(equalTo: bottomAnchor),
stocksView.leadingAnchor.constraint(equalTo: leadingAnchor),
stocksConstraint,
bondsView.topAnchor.constraint(equalTo: topAnchor),
bondsView.bottomAnchor.constraint(equalTo: bottomAnchor),
bondsView.leadingAnchor.constraint(equalTo: stocksView.trailingAnchor, constant: 2),
bondsConstraint,
etfView.topAnchor.constraint(equalTo: topAnchor),
etfView.bottomAnchor.constraint(equalTo: bottomAnchor),
etfView.leadingAnchor.constraint(equalTo: bondsView.trailingAnchor, constant: 2),
etfConstraint
])
}
}

Swift: Is there a faster, more responsive way to implement different corner radii for a UIView?

Currently, I implement multiple corner radii on my bubbleView which is a UIView by doing something along the lines of:
// First create the bubble view
bubbleView = UIView()
bubbleView.layer.cornerRadius = 4 // set the corner radius of the "smaller" corner style
bubbleView.layer.cornerCurve = .continuous
bubbleView.clipsToBounds = true
bubbleView.backgroundColor = UIColor.systemBlue
// ...
// Update the "mask" of the bubble view to give another type of rounded corners
let maskPath = UIBezierPath(roundedRect:bubbleView.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: 17.0, height: 0.0))
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.cgPath
bubbleView.layer.mask = maskLayer // updates the mask
My issue is I am setting the mask, self.bubbleView.layer.mask = maskLayer, of the bubbleView in the func layoutSubviews() function, which causes a noticable delay, for example, when the device rotates from portrait to landscape mode.
Is there a faster, more efficient way to implement different corner radii for a UIView that responds faster than simply updating the mask in layoutSubviews() ?
You could try using a subview with the larger radius corners...
custom view
clear background
all 4 corners set to Radius of 4
subView with desired background color
set Radius of 17 on desired corners of subView
Here's some sample code:
class MyCustomView: UIView {
// self's background will be .clear
// so we use a custom property to set the
// background of the subView
public var viewColor: UIColor = .clear {
didSet {
subView.backgroundColor = viewColor
}
}
// corners to use larger radius
public var corners: CACornerMask = [] {
didSet {
subView.layer.maskedCorners = corners
}
}
public var smallRadius: CGFloat = 0 {
didSet {
layer.cornerRadius = smallRadius
}
}
public var bigRadius: CGFloat = 0 {
didSet {
subView.layer.cornerRadius = bigRadius
}
}
private let subView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
addSubview(subView)
subView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
subView.topAnchor.constraint(equalTo: topAnchor),
subView.leadingAnchor.constraint(equalTo: leadingAnchor),
subView.trailingAnchor.constraint(equalTo: trailingAnchor),
subView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
// round all 4 corners of self's layer with the small radius
layer.masksToBounds = true
layer.cornerRadius = smallRadius
layer.cornerCurve = .continuous
// subview only specified corners with bigger radius
subView.layer.masksToBounds = true
subView.layer.cornerRadius = bigRadius
subView.layer.cornerCurve = .continuous
subView.layer.maskedCorners = corners
}
}
and a test view controller to demo it:
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let v = MyCustomView()
v.viewColor = .systemBlue
v.smallRadius = 4
v.bigRadius = 17
// set top-left, top-right, bottom-left to use larger radius
v.corners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner]
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
v.heightAnchor.constraint(equalToConstant: 120.0),
v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
}

Swift UIView Subviews not rounding corners in Custom UIView Subclass

Greetings stack overflow.
I am trying to build a "bullseye" type view, using coloured subviews and the corner radius. The problem I have is, only my first subview's corners are getting rounded and the inner views are still squares. The black view is a subview of my custom view. The red view is it's subview, and they yellow view the subview of that. Pretty simple hierarchy.
The result looks like this:
I add the views and set their constraints manually. My test app just has the ThreeCircleView dead center of a view controller with the X,Y centered and the width, height constant. I do the actual rounding of the corners in didLayoutSubViews because the size of the view might change, so the corners would have to be resized.
I wrote a test view to isolate this, here it is
class ThreeCircleView: UIView {
var outerCircle: UIView = UIView()
var middleCircle: UIView = UIView()
var innerCircle: UIView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
addSubViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
translatesAutoresizingMaskIntoConstraints = false
addSubViews()
}
func addSubViews() {
outerCircle.backgroundColor = .black
middleCircle.backgroundColor = .red
innerCircle.backgroundColor = .yellow
self.addSubview(outerCircle)
outerCircle.addSubview(middleCircle)
middleCircle.addSubview(innerCircle)
let outerCenterY = outerCircle.centerYAnchor.constraint(equalTo: self.centerYAnchor)
let outerCenterX = outerCircle.centerXAnchor.constraint(equalTo: self.centerXAnchor)
let outerCenterWidth = outerCircle.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -50.0 )
let outerCenterHeight = outerCircle.heightAnchor.constraint(equalTo: self.heightAnchor, constant: -50.0 )
outerCircle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([outerCenterY,outerCenterX,outerCenterWidth,outerCenterHeight])
self.setNeedsLayout()
let middleCenterY = middleCircle.centerYAnchor.constraint(equalTo: self.centerYAnchor)
let middleCenterX = middleCircle.centerXAnchor.constraint(equalTo: self.centerXAnchor)
let middleCenterWidth = middleCircle.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -100.0 )
let middleCenterHeight = middleCircle.heightAnchor.constraint(equalTo: self.heightAnchor, constant: -100.0 )
middleCircle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([middleCenterY,middleCenterX,middleCenterWidth,middleCenterHeight])
let innerCenterY = innerCircle.centerYAnchor.constraint(equalTo: self.centerYAnchor)
let innerCenterX = innerCircle.centerXAnchor.constraint(equalTo: self.centerXAnchor)
let innerCenterWidth = innerCircle.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -150.0 )
let innerCenterHeight = innerCircle.heightAnchor.constraint(equalTo: self.heightAnchor, constant: -150.0 )
innerCircle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([innerCenterY,innerCenterX,innerCenterWidth,innerCenterHeight])
}
func makeCircle(v:UIView) {
v.layer.cornerRadius = v.frame.size.width * 0.50
v.clipsToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
makeCircle(v: outerCircle)
makeCircle(v: middleCircle)
makeCircle(v: innerCircle)
}
}
An easy way to make it look as expected is to add layoutIfNeeded() call inside your makeCircle(v:UIView) method. This will make you sure that all views' frames are updated correctly before applying visual changes:
func makeCircle(v:UIView) {
v.layoutIfNeeded()
v.layer.cornerRadius = v.frame.size.width * 0.50
v.clipsToBounds = true
}

How can implement two vertical button in swipe to delete in ios?

I am trying to implement swipe to delete feature with two options in tableview, one is to delete and another one is to Update.The things I want is these options should be vertical rather than horizontal.I have checked so many question but nothing find.
Thanks in advance for support.
.
As I mentioned in the comments, here is one approach:
add your buttons to the cell
add a "container" view to the cell
constrain the container view so it overlays / covers the buttons
add a Pan gesture recognizer to the container view so you can drag it left / right
as you drag it left, it will "reveal" the buttons underneath
You lose all of the built-in swipe functionality, but this is one approach that might give you the design you're going for.
First, an example of creating a "drag view":
class DragTestViewController: UIViewController {
let backgroundView = UIView()
let containerView = UIView()
// leading and trailing constraints for the drag view
private var leadingConstraint: NSLayoutConstraint!
private var trailingConstraint: NSLayoutConstraint!
private let origLeading = CGFloat(60.0)
private let origTrailing = CGFloat(-60.0)
private var currentLeading = CGFloat(60.0)
private var currentTrailing = CGFloat(-60.0)
override func viewDidLoad() {
super.viewDidLoad()
backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.backgroundColor = .cyan
backgroundView.clipsToBounds = true
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = .red
// add a label to the container view
let exampleLabel = UILabel()
exampleLabel.translatesAutoresizingMaskIntoConstraints = false
exampleLabel.text = "Drag Me"
exampleLabel.textColor = .yellow
containerView.addSubview(exampleLabel)
backgroundView.addSubview(containerView)
view.addSubview(backgroundView)
leadingConstraint = containerView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: origLeading)
trailingConstraint = containerView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor, constant: origTrailing)
NSLayoutConstraint.activate([
// constrain backgroundView top to top + 80
backgroundView.topAnchor.constraint(equalTo: view.topAnchor, constant: 80.0),
// constrain backgroundView leading / trailing to leading / trailing with 40-pt "padding"
backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40.0),
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40.0),
// constrain height to 100
backgroundView.heightAnchor.constraint(equalToConstant: 100.0),
// constrain containerView top / bottom to backgroundView top / bottom with 8-pt padding
containerView.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: 8.0),
containerView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: -8.0),
// activate leading / trailing constraints
leadingConstraint,
trailingConstraint,
// constrain the example label centered in the container view
exampleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
exampleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
])
// pan gesture recognizer
let p = UIPanGestureRecognizer(target: self, action: #selector(self.drag(_:)))
containerView.addGestureRecognizer(p)
}
#objc func drag(_ g: UIPanGestureRecognizer) -> Void {
// when we get a Pan on the containerView - a "drag" ...
guard let sv = g.view?.superview else {
return
}
let translation = g.translation(in: sv)
switch g.state {
case .began:
// update current vars
currentLeading = leadingConstraint.constant
currentTrailing = trailingConstraint.constant
case .changed:
// only track left-right dragging
leadingConstraint.constant = currentLeading + translation.x
trailingConstraint.constant = currentTrailing + translation.x
default:
break
}
}
}
That code will produce this:
A red view with a centered label, inside a cyan view. You can drag the red "container" view left and right.
Add a view controller to a new project and assign its Custom Class to DragTestViewController from the above code. There are no #IBOutlet or #IBAction connections, so you should be able to run it as-is. See if you can drag the red view.
Using that as a starting point, we can get this:
with this code:
// simple rounded-corner shadowed view
class ShadowRoundedView: UIView {
let shadowLayer: CAShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.layer.addSublayer(shadowLayer)
clipsToBounds = false
backgroundColor = .clear
shadowLayer.fillColor = UIColor.white.cgColor
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowOffset = CGSize(width: 0.0, height: 1.0)
shadowLayer.shadowRadius = 4.0
shadowLayer.shadowOpacity = 0.6
shadowLayer.shouldRasterize = true
shadowLayer.rasterizationScale = UIScreen.main.scale
}
override func layoutSubviews() {
super.layoutSubviews()
let pth = UIBezierPath(roundedRect: bounds, cornerRadius: 16.0)
shadowLayer.path = pth.cgPath
}
}
// simple rounded button
class RoundedButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.height * 0.5
}
}
class DragRevealCell: UITableViewCell {
// callback closure for button taps
var callback: ((Int) -> ())?
// this will hold the "visible" labels, and will initially cover the buttons
let containerView: ShadowRoundedView = {
let v = ShadowRoundedView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// this will hold the buttons
let buttonsView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.clipsToBounds = true
return v
}()
// a "delete" button
let deleteButton: RoundedButton = {
let v = RoundedButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Delete", for: [])
v.setTitleColor(.blue, for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .white
return v
}()
// an "update" button
let updateButton: RoundedButton = {
let v = RoundedButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Update", for: [])
v.setTitleColor(.white, for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .blue
return v
}()
// single label for this example cell
let myLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.numberOfLines = 0
return v
}()
// leading and trailing constraints for the container view
private var leadingConstraint: NSLayoutConstraint!
private var trailingConstraint: NSLayoutConstraint!
private let origLeading = CGFloat(8.0)
private let origTrailing = CGFloat(-8.0)
private var currentLeading = CGFloat(0.0)
private var currentTrailing = CGFloat(0.0)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// cell background color
backgroundColor = UIColor(white: 0.95, alpha: 1.0)
// add buttons to buttons container view
buttonsView.addSubview(deleteButton)
buttonsView.addSubview(updateButton)
// add label to container view -- this is where you would add all your labels, stack views, image views, etc.
containerView.addSubview(myLabel)
// add buttons view first
addSubview(buttonsView)
// add container view second - this will "overlay" it on top of the buttons view
addSubview(containerView)
// containerView leading / trailing constraints - these will be updated as we drag
leadingConstraint = containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: origLeading)
trailingConstraint = containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: origTrailing)
// needed to avoid layout warnings
let bottomConstraint = containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0)
bottomConstraint.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
leadingConstraint,
trailingConstraint,
bottomConstraint,
myLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0),
myLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
myLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
myLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -8.0),
myLabel.heightAnchor.constraint(equalToConstant: 120.0),
buttonsView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
buttonsView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
deleteButton.topAnchor.constraint(equalTo: buttonsView.topAnchor, constant: 0.0),
deleteButton.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor, constant: 8.0),
deleteButton.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor, constant: -8.0),
updateButton.bottomAnchor.constraint(equalTo: buttonsView.bottomAnchor, constant: 0.0),
updateButton.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor, constant: 8.0),
updateButton.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor, constant: -8.0),
updateButton.topAnchor.constraint(equalTo: deleteButton.bottomAnchor, constant: 12.0),
updateButton.heightAnchor.constraint(equalTo: deleteButton.heightAnchor),
updateButton.widthAnchor.constraint(equalTo: deleteButton.widthAnchor),
deleteButton.widthAnchor.constraint(equalToConstant: 120.0),
deleteButton.heightAnchor.constraint(equalToConstant: 40.0),
])
// delete button border
deleteButton.layer.borderColor = UIColor.blue.cgColor
deleteButton.layer.borderWidth = 1.0
// targets for button taps
deleteButton.addTarget(self, action: #selector(self.deleteTapped(_:)), for: .touchUpInside)
updateButton.addTarget(self, action: #selector(self.updateTapped(_:)), for: .touchUpInside)
// pan gesture recognizer
let p = UIPanGestureRecognizer(target: self, action: #selector(self.drag(_:)))
containerView.addGestureRecognizer(p)
}
#objc func drag(_ g: UIPanGestureRecognizer) -> Void {
// when we get a Pan on the container view - a "drag" ...
guard let sv = g.view?.superview else {
return
}
let translation = g.translation(in: sv)
switch g.state {
case .began:
currentLeading = leadingConstraint.constant
currentTrailing = trailingConstraint.constant
case .changed:
// only track left-right dragging
// don't allow drag-to-the-right
if currentLeading + translation.x <= origLeading {
leadingConstraint.constant = currentLeading + translation.x
trailingConstraint.constant = currentTrailing + translation.x
}
default:
// if the drag-left did not fully reveal the buttons, animate the container view back in place
if containerView.frame.maxX > buttonsView.frame.minX {
self.leadingConstraint.constant = self.origLeading
self.trailingConstraint.constant = self.origTrailing
UIView.animate(withDuration: 0.3, animations: {
self.layoutIfNeeded()
}, completion: { _ in
//self.dragX = 0.0
})
}
}
}
#objc func deleteTapped(_ sender: Any?) -> Void {
callback?(0)
}
#objc func updateTapped(_ sender: Any?) -> Void {
callback?(1)
}
}
class DragRevealTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(DragRevealCell.self, forCellReuseIdentifier: "DragRevealCell")
tableView.separatorStyle = .none
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "DragRevealCell", for: indexPath) as! DragRevealCell
c.myLabel.text = "Row \(indexPath.row)" + "\n" + "This is where you would populate the cell's labels, image views, any other UI elements, etc."
c.selectionStyle = .none
c.callback = { value in
if value == 0 {
print("Delete action")
} else {
print("Update action")
}
}
return c
}
}
Add a UITableViewController the project and assign its Custom Class to DragRevealTableViewController from the above code. Again, there are no #IBOutlet or #IBAction connections, so you should be able to run it as-is.
NOTE: This is example code only, and should not be considered "production ready"!!! It is only partially implemented and will likely need quite a bit more work. But, it may give you a good starting point.

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