iOS 11, completion of transitionCoordinator.animate() never called - ios

I have wrote a FadeSegue class that override perform function from its super, UIStoryboardSegueclass.
import Foundation
import UIKit
class FadeSegue :UIStoryboardSegue{
override func perform() {
let screenShotView = UIImageView()
screenShotView.image = self.source.view.screenShot
screenShotView.frame = CGRect(x: 0, y: 0, width: self.destination.view.frame.width, height: self.destination.view.frame.height)
self.destination.view.addSubview(screenShotView)
super.perform()
self.source.transitionCoordinator?.animate(alongsideTransition: nil, completion: { (c) in
UIView.animate(withDuration: 0.3, animations: {
screenShotView.alpha = 0
}) { (status) in
screenShotView.removeFromSuperview()
}
})
}
}
extension UIView {
var screenShot: UIImage? {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 1.0);
if let _ = UIGraphicsGetCurrentContext() {
drawHierarchy(in: bounds, afterScreenUpdates: true)
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return screenshot
}
return nil
}
}
When Animates checkbox be unchecked in Attribute Inspector, This code will work fine in iOS 10. But in iOS 11, completion block of transitionCoordinator.animate will never called and fade animation will freeze in middle of its progress.
I cannot find out what is wrong in my code for iOS 11.

Related

self dismiss does not work on iOS 12 but works on higher versions

I have a small snippet of code to dismiss the presented VC:
UIView.animate(withDuration: 0.3, animations: { [weak self] in
guard let self = self else {
return
}
DispatchQueue.main.async {
self.stackViewMain.frame = CGRect(origin: CGPoint(x: self.stackViewMain.frame.origin.x, y: self.view.bounds.height), size: self.stackViewMain.bounds.size)
}
}) { _ in
weak var weakSelf = self
self.dismiss(animated: true, completion: {
weakSelf?.viewModel.animationdDidFinishControllerWillDismiss()
})
}
So the issue is in this part:
self.dismiss(animated: true, completion: {
weakSelf?.viewModel.animationdDidFinishControllerWillDismiss()
})
I just found out that self.dismiss is not being run and completionHandler is not being fired on iOS 12 but everything works as expected on iOS 13.
Can someone explain me why it does not work and how can I fix it?
Animation part is wrong.
You don't need to use DispatchQueue. Animation is always on main thread, Read this
Remove all weak self. Animations and completion are not retained by self so there is no risk of strong retain cycle. Read this
I have tested this on iOS 12 and everything works fine. See below example.
You just show a snipper of code, I guess there is retain cycle exist so you cannot dismiss current VC. You may use Debug Memory graph in Xcode to check it.
import UIKit
class ViewControllerA: UIViewController {
lazy var dismissButton: UIButton = {
let bt = UIButton(frame: CGRect(x: 0, y: 0, width: view.frame.size.width / 4, height: view.frame.size.width / 6.5))
bt.setTitle("Click", for: .normal)
bt.backgroundColor = .systemRed
bt.layer.cornerRadius = 12
bt.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
return bt
}()
lazy var aView: UIView = {
let v = UIView(frame: CGRect(x: 0, y: 50, width: view.frame.size.width / 2, height: view.frame.size.width / 2))
v.backgroundColor = .brown
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGreen
view.addSubview(dismissButton)
view.addSubview(aView)
dismissButton.center = view.center
aView.center.x = self.view.center.x
}
#objc func buttonTapped() {
UIView.animate(withDuration: 2, animations: {
self.aView.frame.size.width /= 2
self.aView.frame.size.height /= 2
}) { finished in
self.dismiss(animated: true, completion: {
print("dismiss")
})
}
}
}

Trying to mimic the Mail.app compose animation keeping a layer in view

I have been trying for a while but I cannot figure out how to create a Compose animation seen in the iOS 10+ when you can drag the new composed email down, then it stays on the bottom and the rest of the app is normally accessed, then when you tap it, it re-shows.
I have created a sample project in which I have a UIViewController that presents another UIViewController which has a UIPanGestureRecognizer in it's UINavigationController that fires the pangesture state analyzer.
I can indeed drag to dismiss it , but I cannot find a way to keep it frame.
Bellow there's a print screen of what I'm trying to accomplish and then my used code to where I'm stuck at.
UIViewController that is the presentingViewController class
//
// ViewController.swift
// dismissLayerTest
//
// Created by Ivan Cantarino on 27/09/17.
// Copyright © 2017 Ivan Cantarino. All rights reserved.
//
import UIKit
class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
#objc let interactor = Interactor()
lazy var presentButton: UIButton = {
let b = UIButton(type: .custom)
b.setTitle("Present", for: .normal)
b.setTitleColor(.black, for: .normal)
b.addTarget(self, action: #selector(didTapPresentButton), for: .touchUpInside)
return b
}()
lazy var testbutton: UIButton = {
let b = UIButton(type: .custom)
b.setTitle("test", for: .normal)
b.setTitleColor(.black, for: .normal)
b.addTarget(self, action: #selector(test), for: .touchUpInside)
return b
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.backgroundColor = .white
view.addSubview(presentButton)
presentButton.anchor(top: nil, left: nil, bottom: nil, right: nil, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 100, height: 100)
presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
presentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
view.addSubview(testbutton)
testbutton.anchor(top: nil, left: nil, bottom: presentButton.topAnchor, right: nil, paddingTop: 0, paddinfLeft: 0, paddingBottom: 100, paddingRight: 0, width: 100, height: 100)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#objc func didTapPresentButton() {
let presentedVC = PresentedViewController()
let navController = UINavigationController(rootViewController: presentedVC)
navController.transitioningDelegate = self
presentedVC.interactor = interactor // new
navController.modalPresentationStyle = .custom
navController.view.layer.masksToBounds = true
present(navController, animated: true, completion: nil)
}
#objc func test() {
print("test")
}
// Handles the presenting animation
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomAnimationForPresentor()
}
// Handles the dismissing animation
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomAnimationForDismisser()
}
// interaction controller, only for dismissing the view;
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
// delegate do custom modal presentation style
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return CustomPresentationController(presentedViewController: presented, presenting: presenting)
}
}
UIViewController 2 that is the presentedViewController
import Foundation
import UIKit
class PresentedViewController: UIViewController, UIViewControllerTransitioningDelegate, UIGestureRecognizerDelegate {
#objc var interactor: Interactor? = nil
#objc var panGr = UIPanGestureRecognizer()
#objc var panTapRecon = UITapGestureRecognizer()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
let leftB = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapCancel))
navigationItem.leftBarButtonItem = leftB
panGr = UIPanGestureRecognizer(target: self, action: #selector(handleGesture))
navigationController?.navigationBar.addGestureRecognizer(panGr)
panTapRecon = UITapGestureRecognizer(target: self, action: #selector(handleNavControllerTapGR))
navigationController?.navigationBar.addGestureRecognizer(panTapRecon)
}
#objc func didTapCancel() {
guard let interactor = interactor else { return }
interactorFinish(interactor: interactor)
dismiss(animated: true, completion: nil)
}
#objc func handleNavControllerTapGR(_ sender: UITapGestureRecognizer) {
print("tap detected")
}
// Swipe gesture recognizer handler
#objc func handleGesture(_ sender: UIPanGestureRecognizer) {
//percentThreshold: This variable sets how far down the user has to drag
//in order to trigger the modal dismissal. In this case, it’s set to 40%.
let percentThreshold:CGFloat = 0.30
// convert y-position to downward pull progress (percentage)
let translation = sender.translation(in: view)
let verticalMovement = translation.y / view.bounds.height
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)
guard let interactor = interactor else { return }
switch sender.state {
case .began:
interactor.hasStarted = true
self.dismiss(animated: true, completion: nil)
case .changed:
// alterar se o tamanho do presentigViewController (MainTabBarController) for alterado no background
let scaleX = 0.95 + (progress * (1 - 0.95))
let scaleY = 0.95 + (progress * (1 - 0.95))
// Não deixa ultrapassar os 100% de scale (tamanho original)
if (scaleX > 1 && scaleY > 1) { return }
presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: scaleX, y: scaleY);
presentingViewController?.view.layer.masksToBounds = true
interactor.shouldFinish = progress > percentThreshold
interactor.update(progress)
case .cancelled:
interactor.hasStarted = false
interactor.cancel()
case .ended:
interactor.hasStarted = false
if (interactor.shouldFinish) {
interactorFinish(interactor: interactor)
} else {
// repõe o MainTabBarController na posição dele atrás do NewPostController
UIView.animate(withDuration: 0.5, animations: {
self.presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: 0.95, y: 0.95);
self.presentingViewController?.view.layer.masksToBounds = true
let c = UIColor.black.withAlphaComponent(0.4)
let shadowView = self.presentingViewController?.view.viewWithTag(999)
shadowView?.backgroundColor = c
})
interactor.cancel()
}
default: break
}
}
#objc func interactorFinish(interactor: Interactor) {
removeShadow()
interactor.finish()
}
// remove a shadow view
#objc func removeShadow() {
UIView.animate(withDuration: 0.2, animations: {
self.presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: 1.0, y: 1.0);
self.presentingViewController?.view.layer.masksToBounds = true
}) { _ in
}
}
}
Here's an Helper file that has the custom presentations:
//
// Helper.swift
// dismissLayerTest
//
// Created by Ivan Cantarino on 27/09/17.
// Copyright © 2017 Ivan Cantarino. All rights reserved.
//
import Foundation
import UIKit
class Interactor: UIPercentDrivenInteractiveTransition {
#objc var hasStarted = false
#objc var shouldFinish = false
}
extension UIView {
#objc func anchor(top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?, paddingTop: CGFloat, paddinfLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, width: CGFloat, height: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
}
if let left = left {
leftAnchor.constraint(equalTo: left, constant: paddinfLeft).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
}
if let right = right {
rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
}
if width != 0 {
widthAnchor.constraint(equalToConstant: width).isActive = true
}
if height != 0 {
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
#objc func roundCorners(corners:UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
}
class CustomAnimationForDismisser: NSObject, UIViewControllerAnimatedTransitioning {
// Tempo da animação
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.27
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get the set of relevant objects.
let containerView = transitionContext.containerView
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {
print("Returning animateTransition VC")
return
}
// from view só existe no dismiss
guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else {
print("Failed to instantiate fromView: CustomAnimationForDismisser()")
return
}
// Set up some variables for the animation.
let containerFrame: CGRect = containerView.frame
var fromViewFinalFrame: CGRect = transitionContext.finalFrame(for: fromVC)
fromViewFinalFrame = CGRect(x: 0, y: containerFrame.size.height, width: containerFrame.size.width, height: containerFrame.size.height)
// Animate using the animator's own duration value.
UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: {
fromView.frame = fromViewFinalFrame
}) { (finished) in
let success = !(transitionContext.transitionWasCancelled)
// Notify UIKit that the transition has finished
transitionContext.completeTransition(success)
}
}
}
class CustomAnimationForPresentor: NSObject, UIViewControllerAnimatedTransitioning {
// Tempo da animação
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get the set of relevant objects.
let containerView = transitionContext.containerView
// obtém os VCs para não o perder na apresentação (default desaparece por trás)
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {//, let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
print("Returning animateTransition VC")
return
}
// gets the view of the presented object
guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else { return }
// Set up animation parameters.
toView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height)
// Always add the "to" view to the container.
containerView.addSubview(toView)
// Animate using the animator's own duration value.
UIView.animate(withDuration: 0.35, delay: 0, options: .curveEaseOut, animations: {
// Zooms out da MainTabBarController - o VC
fromVC.view.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
// propriedades declaradas no CustomPresentationController() // Anima o presented view
toView.transform = .identity
}, completion: { (finished) in
let success = !(transitionContext.transitionWasCancelled)
// So it avoids view stacks and overlap issues
if (!success) { toView.removeFromSuperview() }
// Notify UIKit that the transition has finished
transitionContext.completeTransition(success)
})
}
}
class CustomPresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController!) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
// Tamanho desejado para o NewPostController
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerBounds = containerView?.bounds else {
print("Failed to instantiate container bounds: CustomPresentationController")
return .zero
}
return CGRect(x: 0.0, y: 0.0, width: containerBounds.width, height: containerBounds.height)
}
// Garante que o frame do view controller a mostrar, se mantém conforme desenhado na função frameOfPresentedViewInContainerView
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
}
This desired effect can also be seen in other apps, such like Music app, Stack Exchange/Overflow iOS App
Does anyone have a hint on how can this be accomplished? I feel like I'm really close to achieve it, but I can't find a way to keep the dismissed view with a layer on screen.
The project above can be found here
Thank you very much.
Regards.
I would suggest that Apple (in the animated screen gif you have so helpfully provided) is not using a presented view controller. If it were, the presenting view controller would not be able to shrink its view — and on dismissal, the presented view controller's view would completely disappear.
I would say that underlying this interface is a parent view controller with multiple child view controllers (or maybe just a normal view controller with two child views). Thus, we can display the two child views wherever and however we like. Your animated gif shows two possible arrangements of the two child views: overlapping, and one above the other with the second view just barely visible from the bottom of the screen.

(Swift 3) - How to include animations in custom UIView

I have a UIView subclass called View, in which I want a "ripple" to move outwards from wherever the user double clicks (as well as preforming other drawing functions). Here is the relevant part of my draw method, at present:
override func draw(_ rect: CGRect) {
for r in ripples {
r.paint()
}
}
This is how r.paint() is implemented, in class Ripple:
func paint() {
let c = UIColor(white: CGFloat(1.0 - (Float(iter))/100), alpha: CGFloat(1))
print(iter, " - ",Float(iter)/100)
c.setFill()
view.fillCircle(center: CGPoint(x:x,y:y), radius: CGFloat(iter))
}
iter is supposed to be incremented every 0.1 seconds by a Timer which is started in the constructor of Ripple:
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: move)
move is implemented as follows:
func move(_ timer: Timer) {
while (tier<100) {
iter += 1
DispatchQueue.main.async {
self.view.setNeedsDisplay()
}
}
timer.invalidate()
}
What is supposed to happen is that every 0.1 seconds when the timer fires, it will increment iter and tell the View to repaint, which it will then do using the Ripple.paint() method, which uses iter to determine the color and radius of the circle.
However, as revealed using print statements, what happens instead is that the Timer fires all 100 times before the redraw actually takes place. I have tried dealing with this by replacing DispatchQueue.main.async with DispatchQueue.main.sync, but this just made the app hang. What am I doing wrong? If this is just not the right way to approach the problem, how can I get a smooth ripple animation (which involves a circle which grows while changing colors) to take place while the app can still preform other functions such as spawning more ripples? (This means that multiple ripples need to be able to work at once.)
You can achieve what you want with CABasicAnimation and custom CALayer, like that:
class MyLayer : CALayer
{
var iter = 0
override class func needsDisplay(forKey key: String) -> Bool
{
let result = super.needsDisplay(forKey: key)
return result || key == "iter"
}
override func draw(in ctx: CGContext) {
UIGraphicsPushContext(ctx)
UIColor.red.setFill()
ctx.fill(self.bounds)
UIGraphicsPopContext()
NSLog("Drawing, \(iter)")
}
}
class MyView : UIView
{
override class var layerClass: Swift.AnyClass {
get {
return MyLayer.self
}
}
}
class ViewController: UIViewController {
let myview = MyView()
override func viewDidLoad() {
super.viewDidLoad()
myview.frame = self.view.bounds
self.view.addSubview(myview)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let animation = CABasicAnimation(keyPath: "iter")
animation.fromValue = 0
animation.toValue = 100
animation.duration = 10.0
myview.layer.add(animation, forKey: "MyIterAnimation")
(myview.layer as! MyLayer).iter = 100
}
}
Custom UIView like AlertView with Animation for Swift 3 and Swift 4
You can use a extension:
extension UIVIew{
func customAlertView(frame: CGRect, message: String, color: UIColor, startY: CGFloat, endY: CGFloat) -> UIVIew{
//Adding label to view
let label = UILabel()
label.frame = CGRect(x: 0, y: 0, width: frame.width, height:70)
label.textAlignment = .center
label.textColor = .white
label.numberOfLines = 0
label.text = message
self.addSubview(label)
self.backgroundColor = color
//Adding Animation to view
UIView.animate(withDuration:0.5, delay: 0, options:
[.curveEaseOut], animations:{
self.frame = CGRect(x: 0, y: startY, width: frame.width, height: 64)
}) { _ in
UIView.animate(withDuration: 0.5, delay: 4, options: [.curveEaseOut], animations: {
self.frame = CGRect(x: 0, y: endY, width: frame.width, height:64)
}, completion: {_ in
self.removeFromSuperview()
})
}
return self
}
And you can use it un your class. This example startY is when you have a navigationController:
class MyClassViewController: UIViewController{
override func viewDidLoad(){
super.viewDidLoad()
self.view.addSubView(UIView().addCustomAlert(frame: self.view.frame, message: "No internet connection", color: .red, startY:64, endY:-64))
}
}

Reset UIProgressView and begin animating immediately with Swift 3

I have a progress view like the one Snapchat and Instagram Stories have. My content changes after the progress view reaches to the end or when tapped on a button.
I'm reseting the progress view when content changes. Everything works as expected while there is no intervention. But when tapped on next button the progress view doesn't start again until the other loop executes.
You can see the video here quickly.
I came across this question while I was researching, I have the same scenario but I couldn't apply the principle as a newbie with swift 3.
Here is my code, any help would be highly appreciated:
func startTimer(){
self.timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(myVievController.nextItem), userInfo: nil, repeats: false)
}
func updateProgressBar() {
self.progressView.setProgress(1.0, animated: false)
UIView.animate(withDuration: 2.8, animations: {() -> Void in
self.progressView.layoutIfNeeded()
}, completion: { finished in
if finished {
self.progressView.setProgress(0, animated: false)
}
})
}
func nextItem(){
// ...
self.updateUI(item: self.myCurrentItem)
// ...
}
func updateUI(item:MyItem){
self.updateProgressBar()
self.timer.invalidate()
self.startTimer()
// Clear old item and fill new values etc...
}
#IBAction func nextButtonPressed(_ sender: UIButton) {
self.nextItem()
}
Could also do it with a subclass:
class HelloWorld: UIProgressView {
func startProgressing(duration: TimeInterval, resetProgress: Bool, completion: #escaping (Void) -> Void) {
stopProgressing()
// Reset to 0
progress = 0.0
layoutIfNeeded()
// Set the 'destination' progress
progress = 1.0
// Animate the progress
UIView.animate(withDuration: duration, animations: {
self.layoutIfNeeded()
}) { finished in
// Remove this guard-block, if you want the completion to be called all the time - even when the progression was interrupted
guard finished else { return }
if resetProgress { self.progress = 0.0 }
completion()
}
}
func stopProgressing() {
// Because the 'track' layer has animations on it, we'll try to remove them
layer.sublayers?.forEach { $0.removeAllAnimations() }
}
}
This can be used by calling -startProgressing() when ever you need to start the progressView again from the start. The completion is called when it has finished the animation, so you can change the views etc.
An example of use:
progressView.startProgressing(duration: 5.0, resetProgress: true) {
// Set the next things you need... change to a next item etc.
}
You probably need something like this to update your progress bar: self.progressView.setProgress(0, animated: false)
func updateProgressBar() {
DispatchQueue.main.async {
self.progressView.setProgress(0.1, animated: false)
}
if self.progressView.Progress == 1.0 {
self.timer.invalidate()
} else {
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(myVievController.updateProgressBar), userInfo: nil, repeats: false)
}
}
Then you can invalidate the timer and stop the update when you are at 100%
Example
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var progressView: UIProgressView!
var timer : Timer?
var timerCount = 0
var imageArray : Array<UIImage> = []
// MARK: - Lifecycle -
override func viewDidLoad() {
super.viewDidLoad()
// create gradient images
buildImageArray(duration: 3, highlightPercentage: 10.0, mainColor: .green, highlightColor: .yellow, imageWidth: Double(progressView.frame.width))
// set progressView to first image
progressView.progressImage = imageArray[0]
// set progressView progress
progressView.progress = 0.7
// schedule timer
if self.timer == nil {
self.timer = Timer.scheduledTimer(timeInterval: 1/60, target: self, selector: #selector(updateGradient), userInfo: nil, repeats: true)
RunLoop.main.add(self.timer!, forMode: .common)
}
}
func buildImageArray(duration: Int, highlightPercentage: Double, mainColor: UIColor, highlightColor: UIColor, imageWidth: Double){
let keyFrames = duration * 60
for i in 0..<keyFrames {
// Drawing code
let frame = CGRect(x: 0.0, y: 0.0, width: imageWidth, height: 1.0)
let layer = CAGradientLayer()
layer.frame = frame
layer.startPoint = CGPoint(x: 0.0, y: 0.5)
layer.endPoint = CGPoint(x: 1.0, y: 0.5)
var colors : [UIColor] = []
for n in 0..<keyFrames {
colors.append(mainColor)
}
let highlightKeyFrames : Int = Int(floor(Double(keyFrames) * (highlightPercentage/100)))
// 300 * .3 = 90 kf
for x in 0..<highlightKeyFrames {
let p = i+x
if p < keyFrames {
colors[p] = highlightColor
}
}
layer.colors = colors.map { $0.cgColor }
layer.bounds = frame
let image = UIImage.imageWithLayer(layer: layer)
imageArray.append(image)
}
}
// updateGradient
#objc func updateGradient(){
// crop image to match progress
let newWidth = self.progressView.frame.width * CGFloat(progressView.progress)
let progressImage = imageArray[timerCount].crop(rect: CGRect(x: 0, y: 0, width: newWidth, height: 1))
progressView.progressImage = progressImage
// increment timer
timerCount = timerCount + 1
if timerCount >= imageArray.count {
timerCount = 0
}
}
}
extension UIImage {
class func imageWithLayer(layer: CALayer) -> UIImage {
UIGraphicsBeginImageContextWithOptions(layer.bounds.size, layer.isOpaque, 0.0)
layer.render(in: UIGraphicsGetCurrentContext()!)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img!
}
func crop( rect: CGRect) -> UIImage {
var rect = rect
rect.origin.x*=self.scale
rect.origin.y*=self.scale
rect.size.width*=self.scale
rect.size.height*=self.scale
let imageRef = self.cgImage!.cropping(to: rect)
let image = UIImage(cgImage: imageRef!, scale: self.scale, orientation: self.imageOrientation)
return image
}
}

Having some trouble making a custom UIActivityIndicatorView

I am trying my hand at creating a custom activity indicator view programatically. The problem is that it never starts animating. Here is the code for the spinner.swift class:
import UIKit
class spinner: UIActivityIndicatorView {
var flag = Bool()
override init(frame: CGRect) {
super.init(frame: frame)
self.flag = true
self.isHidden = false
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func startAnimating() {
self.animate()
}
func animate()
{
if flag == true
{
UIView.animate(withDuration: 0.3, animations: {
self.layer.setAffineTransform(CGAffineTransform(scaleX: 0.5, y: 1))
}) { (success) in
if success == true
{
UIView.animate(withDuration: 0.3, animations: {
self.layer.setAffineTransform(CGAffineTransform.identity)
}, completion: { (success) in
if success == true
{
self.animate()
}
})
}
}
}
}
override func stopAnimating() {
self.flag = false
}
override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: rect)
UIColor.cyan.setStroke()
path.stroke()
UIColor.red.setFill()
path.fill()
}
}
This is the code in viewDidLoad() where I've added the spinner:
let aiv = spinner(frame: CGRect(x: self.view.bounds.width/2-35, y: self.view.bounds.height/2-35, width: 70, height: 70))
aiv.hidesWhenStopped = true
self.view.addSubview(aiv)
aiv.startAnimating()
print(aiv.isAnimating)
print(air)
I don't see a spinner at all, and get the following message in the console:
false
<spinner.spinner: 0x7f82b3e08240; baseClass = UIActivityIndicatorView; frame = (170 298.5; 35 70); transform = [0.5, 0, 0, 1, 0, 0]; hidden = YES; animations = { transform=<CABasicAnimation: 0x6080000364a0>; }; layer = <CALayer: 0x608000035120>>
According to the logs, the spinner is hidden, which means it never started animating.
It would be great if anyone could point out where I'm going wrong, and suggest a possible fix.
Thanks!
Your problem appears because of the call:
aiv.hidesWhenStopped = true
And the property isAnimating returns false because you are overriding the animate method and not use the foundation one.
You should set this property in your custom class when you are starting and stopping your custom animation.
The same case is the hidesWhenStopped. This should be implemented by you in your class.
I would also recommend to use UIView as a subclass with a UIActivityIndicator inside because if you want to start the ActivityIndicator the AffineTransformation could disrupt each other.

Resources