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
Related
I am using custom control to trigger the presentation of a UIColorPickerViewController. It triggers the color picker, but not as expected.
I was using UIColorWell previously, which does present the picker correctly (but it is an opaque implementation so I don't know how it does so). I don't want to use UIColorWell, because the shape and appearance aren't right for where I'm invoking the picker from.
Everything works exactly as desired except I can't get the color picker to present at the bottom of the screen like UIColorWell would do it. Instead, whatever I've tried to present (or show), UIColorPickerController appears near the top of the screen.
Note: Currently, to present the color picker controller, I'm using the latest iOS 15 trick with sheet controller / detents to try to anchor the picker to the bottom of the screen, but it's not working as expected (it's the technique is from the Apple Docs example, and as I've seen documented online).
What might be happening? What can I do to get the picker at the bottom of the screen, so that it doesn't obstruct my interface?
The Extension:
extension UIView {
func findViewController() -> UIViewController? {
if let nextResponder = self.next as? UIViewController {
return nextResponder
} else if let nextResponder = self.next as? UIView {
return nextResponder.findViewController()
} else {
return nil
}
}
}
Reproducible example (almost. It needs to be implemented/invoked from whatever VC [not shown here]):
This is custom control that that creates a rectangular color picker view (I'm using it to replace UIColorWell). It attempts to locate the VC which contains the view of which this custom control is a subview.
import UIKit
import QuartzCore
class RectangularColorWell : UIControl {
var colorPickerController = UIColorPickerViewController()
var selectedColor : UIColor = UIColor.clear
var lineWidth : CGFloat = 4.0 {
didSet {
self.setNeedsDisplay()
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
colorPickerController.supportsAlpha = false
colorPickerController.delegate = self
selectedColor = backgroundColor ?? UIColor.clear
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(CustomToggleControl.controlTapped(_:)))
tapGestureRecognizer.numberOfTapsRequired = 1;
tapGestureRecognizer.numberOfTouchesRequired = 1;
self.addGestureRecognizer(tapGestureRecognizer)
self.setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.setLineWidth(lineWidth)
ctx.setFillColor(backgroundColor!.cgColor)
ctx.fill(rect)
drawGradientBorder(rect, context: ctx)
}
func drawGradientBorder(_ rect: CGRect, context: CGContext) {
context.saveGState()
context.setLineWidth(lineWidth)
let path = UIBezierPath(rect: rect)
context.addPath(path.cgPath)
context.replacePathWithStrokedPath()
context.clip()
let rainbowColors : [UIColor] = [ .red, .orange, .yellow, .green, .blue, .purple ]
let colorDistribution : [CGFloat] = [ 0.0, 0.2, 0.4, 0.6, 0.8, 1.0 ]
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: rainbowColors.map { $0.cgColor } as CFArray, locations: colorDistribution)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0), end: CGPoint(x: rect.width, y: rect.height), options: [])
context.restoreGState()
}
#objc func controlTapped(_ gestureRecognizer :UIGestureRecognizer) {
UIView.animate(withDuration: 0.2, delay: 0.0, options: UIView.AnimationOptions.curveEaseIn, animations: {
self.alpha = 0.5
}, completion: { (finished: Bool) -> Void in
UIView.animate(withDuration: 0.2, delay: 0.0, options: UIView.AnimationOptions.curveEaseOut, animations: {
self.alpha = 1.0
}, completion: { (finished: Bool) -> Void in
})
})
setNeedsDisplay()
let vc = self.findViewController()
if let sheet = vc?.sheetPresentationController {
sheet.detents = [.medium()]
sheet.largestUndimmedDetentIdentifier = .medium
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
sheet.prefersEdgeAttachedInCompactHeight = true
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
}
vc?.present(colorPickerController, animated: true, completion: nil)
sendActions(for: .touchDown)
}
}
extension RectangularColorWell : UIColorPickerViewControllerDelegate {
func colorPickerViewControllerDidFinish(_ controller : UIColorPickerViewController) {
self.selectedColor = controller.selectedColor
}
func colorPickerViewController(_ controller : UIColorPickerViewController, didSelect color: UIColor, continuously: Bool) {
self.backgroundColor = color
self.setNeedsDisplay()
}
}
#JordanH's final comment caused me to read up on the show/present methods on UIViewController docs. The default presentation style was what I'd been using (e.g. I hadn't set any of the presentation options on the color picker's view controller when I filed the question.
Surprisingly after reading that I could control the style, setting the picker's modalPresentationStyle property to '.pageSheet' which is the one that looks like it would work, to do a partial covering, did not work!.
The solution turned out to be adding colorPickerController.modalPresentationStyle = .popover before calling present()in the custom RectangularColorWellView). It now behaves as if it was presented from a UIColorWell control view.
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.
Hi I want to make animation with 3 UIView. The main problem is I can't start animation once it's stopped by removing animation from layer.
Here is the code:
class HTAnimatedTypingView: UIView {
#IBOutlet weak var view1: UIView!
#IBOutlet weak var view2: UIView!
#IBOutlet weak var view3: UIView!
func startAnimation(){
UIView.animate(withDuration: 0.5, delay: 0, options: .repeat, animations: {
self.view1.frame.origin.y = 0
}, completion: nil)
UIView.animate(withDuration: 0.3, delay: 0.5, options: .repeat, animations: {
self.view2.frame.origin.y = 0
}, completion: nil)
UIView.animate(withDuration: 0.2, delay: 1.0, options: .repeat, animations: {
self.view3.frame.origin.y = 0
}, completion: nil)
}
func stopAnimations(){
self.view1.layer.removeAllAnimations()
self.view2.layer.removeAllAnimations()
self.view3.layer.removeAllAnimations()
}
}
Output of Above Code:
Expected Animation:
How can make it work with start animation & stop animation functionality? Thanks in advance...
Since you need to add some pause in between each sequence of animations, I would personally do it using key frames as it gives you some flexibility:
class AnimationViewController: UIViewController {
private let stackView: UIStackView = {
$0.distribution = .fill
$0.axis = .horizontal
$0.alignment = .center
$0.spacing = 10
return $0
}(UIStackView())
private let circleA = UIView()
private let circleB = UIView()
private let circleC = UIView()
private lazy var circles = [circleA, circleB, circleC]
func animate() {
let jumpDuration: Double = 0.30
let delayDuration: Double = 1.25
let totalDuration: Double = delayDuration + jumpDuration*2
let jumpRelativeDuration: Double = jumpDuration / totalDuration
let jumpRelativeTime: Double = delayDuration / totalDuration
let fallRelativeTime: Double = (delayDuration + jumpDuration) / totalDuration
for (index, circle) in circles.enumerated() {
let delay = jumpDuration*2 * TimeInterval(index) / TimeInterval(circles.count)
UIView.animateKeyframes(withDuration: totalDuration, delay: delay, options: [.repeat], animations: {
UIView.addKeyframe(withRelativeStartTime: jumpRelativeTime, relativeDuration: jumpRelativeDuration) {
circle.frame.origin.y -= 30
}
UIView.addKeyframe(withRelativeStartTime: fallRelativeTime, relativeDuration: jumpRelativeDuration) {
circle.frame.origin.y += 30
}
})
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
circles.forEach {
$0.layer.cornerRadius = 20/2
$0.layer.masksToBounds = true
$0.backgroundColor = .systemBlue
stackView.addArrangedSubview($0)
$0.widthAnchor.constraint(equalToConstant: 20).isActive = true
$0.heightAnchor.constraint(equalTo: $0.widthAnchor).isActive = true
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animate()
}
}
It should be pretty straightforward, but feel free to let me know if you have any questions!
And this is how the result looks like:
One way could be to use a Timer. Keep an instance of Timer in your class. When startAnimation is called, schedule it. When stopAnimation is called, invalidate it. (This means that the currently ongoing animation will be completed before the animation actually stops, which IMO makes it a nice non-abrupt stop).
On each tick of the timer, animate the dots once. Note that the animation you apply on each dot should have the same duration, as in the expected output, they all bounce at the same rate, just at different instants in time.
Some illustrative code:
// startAnimation
timer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true) { _ in
self.animateDotsOnce()
}
// stopAnimation
timer.invalidate()
// animateDotsOnce
UIView.animate(withDuration: animationDuration, delay: 0, animations: {
self.view1.frame.origin.y = animateHeight
}, completion: {
_ in
UIView.animate(withDuration: animationDuration) {
self.view1.frame.origin.y = 0
}
})
// plus the other two views, with different delays...
I'll leave it to you to find a suitable animateHeight, timerInterval, animationDuration and delays for each view.
I'd recommend using a CAKeyframeAnimation instead of handling completion blocks and that sorcery. Here's a quick example:
for i in 0 ..< 3 {
let bubble = UIView(frame: CGRect(x: 20 + i * 20, y: 200, width: 10, height: 10))
bubble.backgroundColor = .red
bubble.layer.cornerRadius = 5
self.view.addSubview(bubble)
let animation = CAKeyframeAnimation()
animation.keyPath = "position.y"
animation.values = [0, 10, 0]
animation.keyTimes = [0, 0.5, 1]
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.duration = 1
animation.isAdditive = true
animation.repeatCount = HUGE
animation.timeOffset = CACurrentMediaTime() + 0.2 * Double(i)
bubble.layer.add(animation, forKey: "anim")
}
When you wanna remove the animation you just use bubble.layer.removeAnimation(forKey: "anim"). You might have to play around with the timing function or values and keyTimes to get the exact movement you want. But keyframes is the way to go to make a specific animation.
Side note: this example won't work in viewDidLoad cause the view doesn't have a superview yet so the animation won't work. If you test it in viewDidAppear it will work.
UIView animation is different to CALayer animation,best not to mix them.
Write locally and tested.
import UIKit
import SnapKit
class HTAnimatedTypingView: UIView {
private let view0 = UIView()
private let view1 = UIView()
private let view2 = UIView()
init() {
super.init(frame: CGRect.zero)
makeUI()
}
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
makeUI()
}
private func makeUI() {
backgroundColor = UIColor.white
view0.backgroundColor = UIColor.red
view1.backgroundColor = UIColor.blue
view2.backgroundColor = UIColor.yellow
addSubview(view0)
addSubview(view1)
addSubview(view2)
view0.snp.makeConstraints { (make) in
make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(10)
make.height.equalTo(10)
make.left.equalTo(self.snp.left)
}
view1.snp.makeConstraints { (make) in
make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(10)
make.height.equalTo(10)
make.centerX.equalTo(self.snp.centerX)
}
view2.snp.makeConstraints { (make) in
make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(10)
make.height.equalTo(10)
make.right.equalTo(self.snp.right)
}
}
public func startAnimation() {
let duration:CFTimeInterval = 0.5
let animation_delay:CFTimeInterval = 0.1
assert(duration >= animation_delay * 5, "animation_delay should be way smaller than duration in order to make animation natural")
let translateAnimation = CABasicAnimation(keyPath: "position.y")
translateAnimation.duration = duration
translateAnimation.repeatCount = Float.infinity
translateAnimation.toValue = 0
translateAnimation.fillMode = CAMediaTimingFillMode.both
translateAnimation.isRemovedOnCompletion = false
translateAnimation.autoreverses = true
view0.layer.add(translateAnimation, forKey: "translation")
DispatchQueue.main.asyncAfter(deadline: .now() + animation_delay) { [unowned self ] in
self.view1.layer.add(translateAnimation, forKey: "translation")
}
DispatchQueue.main.asyncAfter(deadline: .now() + animation_delay * 2) { [unowned self ] in
self.view2.layer.add(translateAnimation, forKey: "translation")
}
}
public func stopAnimation() {
self.view0.layer.removeAllAnimations()
self.view1.layer.removeAllAnimations()
self.view2.layer.removeAllAnimations()
}
}
I have a pan gesture recognizer in my app that does a function when swiped down. The animation used to be smooth but all of a sudden (without adding an code to that view controller) it becomes very laggy and I have to swipe down quickly for it to do that animation, It doesn't follow my finger.
What is the cause of that
class PhotoViewController: UIViewController, CLLocationManagerDelegate, UIPickerViewDataSource, UIPickerViewDelegate {
override var prefersStatusBarHidden: Bool {
return true
}
private var backgroundImage: UIImage
private var backgroundImageView: UIImageView?
private var picker: UIPickerView
var list: [String]
var locationPicked: String
var locationArray: [Any]
var partyIndex: Int = Int()
init(image: UIImage) {
self.backgroundImage = image
self.picker = UIPickerView()
self.list = ["Loading..."]
self.locationPicked = list[0]
self.locationArray = []
super.init(nibName: nil, bundle: nil)
print(image)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/************************************/
override func viewDidLoad() {
super.viewDidLoad()
if Reachability.isConnectedToNetwork() == true{
mapApi()
}
let backgroundImageView = UIImageView(frame: view.frame)
backgroundImageView.image = backgroundImage
backgroundImageView.isUserInteractionEnabled = true
self.backgroundImageView = backgroundImageView
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognizerAction(_:)))
backgroundImageView.addGestureRecognizer(panGestureRecognizer)
let longGesture = UILongPressGestureRecognizer(target: self, action: #selector(longTap(sender:)))
backgroundImageView.addGestureRecognizer(longGesture)
view.addSubview(backgroundImageView)
view.backgroundColor = greenColor
picker.dataSource = self
picker.delegate = self
picker.frame = CGRect(x: ((self.view.frame.width)/2)-84, y: (self.view.frame.height)-70, width: 160, height: 40)
picker.clipsToBounds = true
picker.layer.cornerRadius = picker.bounds.size.width/20
picker.layer.borderColor = redColor.cgColor
picker.layer.borderWidth = 0
picker.backgroundColor = UIColor(colorLiteralRed: 217/255, green: 83/255, blue: 79/255, alpha: 0.85)
backgroundImageView.addSubview(picker)
print(list)
}
func panGestureRecognizerAction(_ gesture: UIPanGestureRecognizer){
let translation = gesture.translation(in: view)
if let bgImage = gesture.view{
if list.count > 1{
bgImage.frame.origin.y = translation.y
}
}
if gesture.state == .ended{
let tickName = "Tick.png"
let tickImage = UIImage(named: tickName)
let tick = UIImageView(image: tickImage!)
tick.frame = CGRect(x: ((self.view.frame.width)/2)-25, y: 150, width: 70, height: 70)
self.view.addSubview(tick)
tick.alpha = 0
let crossName = "Cross.png"
let crossImage = UIImage(named: crossName)
let cross = UIImageView(image: crossImage!)
cross.frame = CGRect(x: ((self.view.frame.width)/2)-25, y: (self.view.frame.height)-75, width: 50, height: 50)
self.view.addSubview(cross)
cross.alpha = 0
let velocity = gesture.velocity(in: view)
if (gesture.view?.frame.origin.y)! > CGFloat(100) || velocity.y > 1500 {
UIView.animate(withDuration: 0.4, animations: {
self.view.backgroundColor = greenColor
gesture.view?.frame.origin = CGPoint(x: 0, y: 275)
tick.alpha = 1
}, completion: { (true) in
UIView.animate(withDuration: 0.4, delay: 0.2, animations: {
tick.alpha = 0
})
if partyAt != nil{
self.uploadPartyPost()
} else{
self.uploadPost()
}
})
UIView.animate(withDuration: 0.5, delay: 1, animations: {
gesture.view?.frame.origin = CGPoint(x: 0, y: 0)
}, completion: { (true) in
self.dismiss(animated: true, completion: nil)
})
//send image here tick
} else if (gesture.view?.frame.origin.y)! < CGFloat(-80) || velocity.y > 1500 {
UIView.animate(withDuration: 0.2, animations: {
self.view.backgroundColor = redColor
gesture.view?.frame.origin = CGPoint(x: 0, y: -150)
cross.alpha = 1
}, completion: { (true) in
UIView.animate(withDuration: 0.5, delay: 0.7, animations: {
cross.alpha = 0
})
})
UIView.animate(withDuration: 0.2, delay: 1, animations: {
gesture.view?.frame.origin = CGPoint(x: 0, y: 0)
}, completion: { (true) in
self.dismiss(animated: true, completion: nil)
})
print("Done")
//send image here cross
} else {
UIView.animate(withDuration: 0.3, animations: {
gesture.view?.frame.origin = CGPoint(x: 0, y: 0)
})
}
}
}
After playing with translation of Pan gesture you need to set translation point back to zero.
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)
}
}