Custom UIView class is not animating as expected - ios

I have a custom UIView class that creates a square with a red background and I'm trying to get a green square slide on top of it from the left. I have a button from the main view that brings me to the screen above but the green screen doesn't animate over it. It just appears at the end result. I do know that I am creating these shapes when I change the initial width of the green square but no indication of an animation.
MainViewController.swift
private func configureAnimatedButton() {
//let sampleAnimatedButton
let sampleAnimatedButton = AnimatedButtonView()
sampleAnimatedButton.center = CGPoint(x: self.view.frame.size.width / 2,
y: self.view.frame.size.height / 2)
self.view.addSubview(sampleAnimatedButton)
sampleAnimatedButton.animateMiddleLayer()
}
AnimatedButton.swift
import Foundation
import UIKit
public class AnimatedButtonView: UIView {
//initWithFrame to init view from code
let movingLayer = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
//initWithCode to init view from xib or storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
//common func to init our view
private func setupView() {
//backgroundColor = .red
self.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
self.backgroundColor = .red
movingLayer.frame = CGRect(x: self.frame.maxX, y: self.frame.minY, width: 0, height: 300)
movingLayer.backgroundColor = .green
self.addSubview(movingLayer)
}
public func animateMiddleLayer() {
UIView.animate(withDuration: 10.0, delay: 1.2, options: .curveEaseOut, animations: {
self.layoutIfNeeded()
var grayLayerTopFrame = self.movingLayer.frame
grayLayerTopFrame.origin.x = 0
grayLayerTopFrame.size.width += 300
self.movingLayer.frame = grayLayerTopFrame
self.layoutIfNeeded()
}, completion: { finished in
print("animated")
})
}
}

You should move grayLayerTopFrame outside the UIView.animate(), before it starts. Also, you don't need layoutIfNeeded if you're just animating the frame (you use it when you have constraints).
var grayLayerTopFrame = self.movingLayer.frame
grayLayerTopFrame.origin.x = 0
grayLayerTopFrame.size.width += 300
UIView.animate(withDuration: 10.0, delay: 1.2, options: .curveEaseOut, animations: {
self.layoutIfNeeded()
self.movingLayer.frame = grayLayerTopFrame
self.layoutIfNeeded()
}, completion: { finished in
print("animated")
})
Also, make sure you don't call it in viewDidLoad() -- at that time, the views aren't laid out yet.

Related

Creating a haptic-touch-like button in Swift/Obj-C

I've been playing around with an idea for a button which, when held, expands to reveal other buttons (like the FAB in Android). Without releasing, sliding one's finger down should highlight other buttons, similar to haptic touch's behaviour.
Here's a crude mockup I've created using Drama: https://youtu.be/Iam8Gjv3gqM.
There should also be an option to have a label horizontally next to each button, and the order of buttons should be as shown (with the selected button at the top instead of its usual position).
I already have a class for each button (seen below), but do not know how to achieve this layout/behaviour.
class iDUButton: UIButton {
init(image: UIImage? = nil) {
super.init(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
if let image = image {
setImage(image, for: .normal)
}
backgroundColor = .secondarySystemFill
layer.cornerRadius = 15
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
class iDUBadgeButton: iDUButton {
var badgeLabel = UILabel()
var badgeCount = 0
override init(image: UIImage? = nil) {
super.init(image: image)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
badgeLabel.textColor = .white
badgeLabel.backgroundColor = .systemRed
badgeLabel.textAlignment = .center
badgeLabel.font = .preferredFont(forTextStyle: .caption1)
badgeLabel.alpha = 0
updateBadge()
addSubview(badgeLabel)
}
override func layoutSubviews() {
super.layoutSubviews()
badgeLabel.sizeToFit()
let height = max(18, badgeLabel.frame.height + 5.0)
let width = max(height, badgeLabel.frame.width + 10.0)
badgeLabel.frame = CGRect(x: frame.width - 5, y: -badgeLabel.frame.height / 2, width: width, height: height);
badgeLabel.layer.cornerRadius = badgeLabel.frame.height / 2;
badgeLabel.layer.masksToBounds = true;
}
func setBadgeCount(badgeCount: Int) {
self.badgeCount = badgeCount;
self.updateBadge()
}
func updateBadge() {
if badgeCount != 0 {
badgeLabel.text = "\(badgeCount)"
}
UIView.animate(withDuration: 0.25, animations: {
self.badgeLabel.alpha = self.badgeCount == 0 ? 0 : 1
})
layoutSubviews()
}
}
If anyone could help with achieving this layout, I'd be very grateful! Thanks in advance.
PS: I'm developing the UI in Swift, but will be converting it to Objective-C once complete. If you're interested, the use case is to adapt the button in the top-left of this to offer a selection of different tags.

Animate setFillColor color change in custom UIView

I have a custom UIView called CircleView which is essentially a colored ellipse. The color property I'm using to color the ellipse is rendered using setFillColor on the graphics context. I was wondering if there was a way to animate the color change, because when I run through the animate / transition the color changes immediately instead of being animated.
Example Setup
let c = CircleView()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
UIView.transition(with: c, duration: 5, options: .transitionCrossDissolve, animations: {
c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
c.color = UIColor.yellow // Not animated
}
Circle View
class CircleView : UIView {
var color = UIColor.blue {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
context.addEllipse(in: rect)
context.setFillColor(color.cgColor)
context.fillPath()
}
}
You can use the built in animation support for the layer's backgroundColor.
While the easiest way to make a circle is to make your view a square (using aspect ratio constraints, for instance) and then set the cornerRadius to half the width or height, I assume you want something a bit more advanced, and that is why you used a path.
My solution to this would be something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
// Setup the view, by setting a mask and setting the initial color
private func setup(){
layer.mask = shape
layer.backgroundColor = color.cgColor
}
// Change the path in case our view changes it's size
override func layoutSubviews() {
super.layoutSubviews()
let path = CGMutablePath()
// add an elipse, or what ever path/shapes you want
path.addEllipse(in: bounds)
// Created an inverted path to use as a mask on the view's layer
shape.path = UIBezierPath(cgPath: path).reversing().cgPath
}
// this is our shape
private var shape = CAShapeLayer()
}
Or if you really need a simple circle, just something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup(){
clipsToBounds = true
layer.backgroundColor = color.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height / 2
}
}
Either way, this will animate nicely:
UIView.animate(withDuration: 5) {
self.circle.color = .red
}
Strange things happens!
Your code is ok, you just need to call your animation in another method and asyncronusly
As you can see, with
let c = CircleView()
override func viewDidLoad() {
super.viewDidLoad()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
changeColor()
}
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self.c, duration: 5, options: .transitionCrossDissolve, animations: {
self.c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
self.c.color = UIColor.yellow // Not animated
}
}
}
Work as charm.
Even if you add a button that trigger the color change, when you press the button the animation will work.
I encourage you to set this method in the definition of the CircleView
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self, duration: 5, options: .transitionCrossDissolve, animations: {
self.color = UIColor.red
})
UIView.animate(withDuration: 5) {
self.color = UIColor.yellow
}
}
}
and call it where you want in your ViewController, simply with
c.changeColor()

Animation: continuous heartbeat monitor lines

I have an image of what symbolises a heartbeat
and I'm trying to loop it in my subview so it continuously appears on my view if you understand what i mean. So far I have got it to appear and move across infinitely on my subview however it isn't continuous as it only appears again from the left after it has completely gone out of the view on the right where as I want it to appear again as soon as it has completely entered the view so it symbolises an actual heartbeat. My code looks something like this on my viewDidLoad:
self.heartbeatLoading.frame.origin = CGPoint(x: 0 - heartbeatLoading.frame.size.width, y: 10)
let animation: UIViewAnimationOptions = [.repeat, .curveLinear]
UIView.animate(withDuration: 5.0, delay: 0, options: animation, animations: {
self.heartbeatLoading.frame = self.heartbeatLoading.frame.offsetBy(dx: self.view.frame.width + self.heartbeatLoading.frame.width, dy: 0.0)
}, completion: nil)
Take one UIView and set this class as a super view. Call start animation and stop the animation whenever required.
#IBDesignable class Pulse : UIView
{
var animatingView : UIView?
#IBInspectable var pulseImage: UIImage = UIImage(named: "defaultImage")!{
didSet {
animatingView = UIView(frame: self.bounds)
let imgView = UIImageView(frame : self.bounds)
animatingView?.clipsToBounds = true
animatingView?.addSubview(imgView)
self.addSubview(animatingView!)
}
}
func startAnimationWith(duration : Double = 0.5)
{
if animatingView != nil
{
animatingView?.frame = CGRect(x: 0, y: 0, width: 0, height: (animatingView?.frame.size.height)!)
UIView.animate(withDuration: duration, delay: 0, options: .repeat, animations: {
self.animatingView?.frame = CGRect(x: 0, y: 0, width: (self.animatingView?.frame.size.width)!, height: (self.animatingView?.frame.size.height)!)
}, completion: { (isDone) in
})
}
}
func stopAnimation()
{
if animatingView != nil
{
self.animatingView!.layer.removeAllAnimations()
}
}
override init(frame : CGRect) {
super.init(frame : frame)
}
convenience init() {
self.init(frame:CGRect.zero)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}

animate view in xcode playground not completing

I am trying to make a custom activity view that spins and flips a logo. I am at the very early stages and building it in xcodes playground. I just want the logo to flip vertically, then horizontally, then vertically again and horizontally again so that the logo is how it began initially. The issue im having is that it is only performing the first flip and then nothing else but i cant see why?
This is what my code in the playground looks like at the moment:
import UIKit
import PlaygroundSupport
import QuartzCore
class LogoActivityView: UIView {
var animationLoopCount: Int = 0
var logoImageView = UIImageView()
var logoImage = UIImage()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setLogoImageView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupView() {
self.backgroundColor = .clear
self.isHidden = true
}
func startAnimating() {
self.isHidden = false
animationLoopCount
while animationLoopCount < 2 {
UIView.animate(withDuration: 1.0, delay: 0.1, options: .curveEaseInOut, animations: {() -> Void in
self.logoImageView.layer.transform = CATransform3DMakeRotation(.pi, 1.0, 0.0, 0.0)
}, completion: {(_ finished: Bool) -> Void in
UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveEaseInOut, animations: {
self.logoImageView.layer.transform = CATransform3DMakeRotation(.pi, 0.0, 1.0, 0.0)
}, completion: nil)
})
animationLoopCount += 1
}
}
func setLogoImageView() {
logoImageView = UIImageView(frame: self.frame)
logoImage = UIImage(named: "MemoryBoothLogo.png")!
logoImageView.image = logoImage
logoImageView.contentMode = .scaleAspectFit
logoImageView.backgroundColor = .clear
self.addSubview(logoImageView)
}
}
let containerView = LogoActivityView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
PlaygroundPage.current.liveView = containerView
PlaygroundPage.current.needsIndefiniteExecution = true
containerView.startAnimating()
Any help would be great thanks

CGAffineTransformMakeScale on circular UIView animate as a square

PopUpMask is a circular UIView. I use it to mask a menu. When the user clicks on a button the menu "pop" aka become a bigger circle. When the user close it, the menu scale down to a smaller circle. The animation works when I scale it up but when I want to scale it down it transform to a square during animation. How can it scale down as a circle?
Here's the UIView scaled:
class PopUpMask: UIView {
override init(frame: CGRect) {
super.init(frame: CGRectMake(0, 0, 40, 40))
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
self.layer.cornerRadius = 20
}
}
These are the methods called:
func unmaskToolbarBackground() {
UIView.animateWithDuration(0.3, delay: 0.3, options: .CurveEaseOut, animations: {
self.popUpMask.transform = CGAffineTransformMakeScale(12, 12)
self.toolbarView.maskView? = self.popUpMask
}, completion: nil)
}
func maskToolbarBackground() {
UIView.animateWithDuration(0.4, delay: 0, options: .CurveEaseOut, animations: {
self.popUpMask.transform = CGAffineTransformMakeScale(0.02, 0.02)
self.toolbarView.maskView? = self.popUpMask
}, completion: nil)
}
If I got it right, you should modify this:
override func drawRect(rect: CGRect) {
self.layer.cornerRadius = 20
}
to be like this:
override func drawRect(rect: CGRect) {
self.layer.cornerRadius = 20
self.layer.masksToBounds = true
}
P.S. A little bit off the record: you no longer have to use self to access
"own" methods in the Obj-C way unless you have a name collision.

Resources