Programmatically created UIView's frame is 0 - ios

Cannot access uiview's frame after setting it's layout. Frame, and center is given as 0,0 points.
I should also mention that there are no storyboard's in this project. All views and everything are created programmatically.
I have created a UIView resultView programmatically and added it as a subview in scrollView, which is also added as a subview of view, then set it's constraints, anchors in a method called setupLayout() I call setupLayout() in viewDidLoad() and after that method, I call another method called configureShapeLayer(). Inside configureShapeLayer() I try to access my view's center as:
let center = resultView.center // should give resultView's center but gives 0
Then by using this center value I try to add two bezierPaths to have a status bar kind of view. But since resultView's center is not updated at that point, it appears as misplaced. Please see the pic below:
I also tried calling setupLayout() in loadView() then calling configureShapeLayer() in viewDidLoad() but nothing changed.
So I need a way to make sure all views are set in my view, all constraints, and layouts are applied before calling configureShapeLayer(). But how can I do this?
I also tried calling configureShapeLayer() in both viewWillLayoutSubviews() and viewDidLayoutSubviews() methods but it made it worse, and didnt work either.
Whole View Controller File is given below: First views are declared, then they are added into the view in prepareUI(), at the end of prepareUI(), another method setupLayout() is called. After it completes setting layout, as can be seen from viewDidLoad, finally configureShapeLayer() method is called.
import UIKit
class TryViewController: UIViewController {
let score: CGFloat = 70
lazy var percentage: CGFloat = {
return score / 100
}()
// MARK: View Declarations
private let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.backgroundColor = .white
return scrollView
}()
private let iconImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}()
let scoreLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
let percentageLabel: UILabel = {
let label = UILabel()
label.text = ""
label.textAlignment = .center
label.font = TextStyle.systemFont(ofSize: 50.0)
return label
}()
// This one is the one should have status bar at center.
private let resultView: UIView = {
let view = UIView()
view.backgroundColor = .purple
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
prepareUI()
configureShapeLayer()
}
private func prepareUI() {
resultView.addSubviews(views: percentageLabel)
scrollView.addSubviews(views: iconImageView,
resultView)
view.addSubviews(views: scrollView)
setupLayout()
}
private func setupLayout() {
scrollView.fillSuperview()
iconImageView.anchor(top: scrollView.topAnchor,
padding: .init(topPadding: 26.0))
iconImageView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 0.31).isActive = true
iconImageView.heightAnchor.constraint(equalTo: iconImageView.widthAnchor, multiplier: 0.67).isActive = true
iconImageView.anchorCenterXToSuperview()
//percentageLabel.frame = CGRect(x: 0, y: 0, width: 105, height: 60)
//percentageLabel.center = resultView.center
percentageLabel.anchorCenterXToSuperview()
percentageLabel.anchorCenterYToSuperview()
let resultViewTopConstraintRatio: CGFloat = 0.104
resultView.anchor(top: iconImageView.bottomAnchor,
padding: .init(topPadding: (view.frame.height * resultViewTopConstraintRatio)))
resultView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 0.533).isActive = true
resultView.heightAnchor.constraint(equalTo: resultView.widthAnchor, multiplier: 1.0).isActive = true
resultView.anchorCenterXToSuperview()
configureShapeLayer()
}
private func configureShapeLayer() {
let endAngle = ((2 * percentage) * CGFloat.pi) - CGFloat.pi / 2
let center = resultView.center // should give resultView's center but gives 0
// Track Layer Part
let trackPath = UIBezierPath(arcCenter: center, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = trackPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor // to make different
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = .round
resultView.layer.addSublayer(trackLayer)
// Score Fill Part
let scorePath = UIBezierPath(arcCenter: center, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: endAngle, clockwise: true)
scoreLayer.path = scorePath.cgPath
scoreLayer.strokeColor = UIColor.red.cgColor
scoreLayer.lineWidth = 10
scoreLayer.fillColor = UIColor.clear.cgColor
scoreLayer.lineCap = .round
scoreLayer.strokeEnd = 0
resultView.layer.addSublayer(scoreLayer)
}
}

You will be much better off creating a custom view. That will allow you to "automatically" update your bezier paths when the view size changes.
It also allows you to keep your drawing code away from your controller code.
Here is a simple example. It adds a button above the "resultView" - each time it's tapped it will increment the percentage by 5 (percentage starts at 5 for demonstration):
//
// PCTViewController.swift
//
// Created by Don Mag on 12/6/18.
//
import UIKit
class MyResultView: UIView {
let scoreLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
var percentage = CGFloat(0.0) {
didSet {
self.percentageLabel.text = "\(Int(percentage * 100))%"
self.setNeedsLayout()
}
}
let percentageLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = ""
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 40.0)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(trackLayer)
layer.addSublayer(scoreLayer)
addSubview(percentageLabel)
NSLayoutConstraint.activate([
percentageLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
percentageLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
override func layoutSubviews() {
super.layoutSubviews()
let endAngle = ((2 * percentage) * CGFloat.pi) - CGFloat.pi / 2
trackLayer.frame = self.bounds
scoreLayer.frame = self.bounds
let centerPoint = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
// Track Layer Part
let trackPath = UIBezierPath(arcCenter: centerPoint, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = trackPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor // to make different
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
// pre-Swift 4.2
trackLayer.lineCap = kCALineCapRound
// trackLayer.lineCap = .round
// Score Fill Part
let scorePath = UIBezierPath(arcCenter: centerPoint, radius: 50, startAngle: -CGFloat.pi / 2, endAngle: endAngle, clockwise: true)
scoreLayer.path = scorePath.cgPath
scoreLayer.strokeColor = UIColor.red.cgColor
scoreLayer.lineWidth = 10
scoreLayer.fillColor = UIColor.clear.cgColor
// pre-Swift 4.2
scoreLayer.lineCap = kCALineCapRound
// scoreLayer.lineCap = .round
}
}
class PCTViewController: UIViewController {
let resultView: MyResultView = {
let v = MyResultView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .purple
return v
}()
let btn: UIButton = {
let b = UIButton()
b.translatesAutoresizingMaskIntoConstraints = false
b.setTitle("Add 5 percent", for: .normal)
b.backgroundColor = .blue
return b
}()
var pct = 5
#objc func incrementPercent(_ sender: Any) {
pct += 5
resultView.percentage = CGFloat(pct) / 100.0
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(btn)
view.addSubview(resultView)
NSLayoutConstraint.activate([
btn.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
btn.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultView.widthAnchor.constraint(equalToConstant: 300.0),
resultView.heightAnchor.constraint(equalTo: resultView.widthAnchor),
resultView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
btn.addTarget(self, action: #selector(incrementPercent), for: .touchUpInside)
resultView.percentage = CGFloat(pct) / 100.0
}
}
Result:

This is not the ideal fix, but try calling configureShapeLayer() func on main thread, like this:
DispatchQueue.main.async {
configureShapeLayer()
}
I had problem like that once and was something like that.

Related

CAShapeLayer with different Colors

I have a CAShapeLayer based on this answer that animates along with a UISlider.
It works fine but as the shapeLayer follows along its just 1 red CAGradientLayer color. What I want is the shapeLayer to change colors based on certain points of the slider. An example is at 0.4 - 0.5 it's red, 0.7-0.8 red, 0.9-0.95 red. Those aren't actual values, the actual values will vary. I figure that any time it doesn't meet the condition to turn red it should probably just be a clear color, which will just show the black track underneath it. The result would look something like this (never mind the shape)
The red colors are based on the user scrubbing the slider and the letting go. The different positions of the slider that determine the red color is based on whatever condition. How can I do this.
UISlider
lazy var slider: UISlider = {
let s = UISlider()
s.translatesAutoresizingMaskIntoConstraints = false
s.minimumTrackTintColor = .blue
s.maximumTrackTintColor = .white
s.minimumValue = 0
s.maximumValue = 1
s.addTarget(self, action: #selector(onSliderChange), for: .valueChanged)
return s
s.addTarget(self, action: #selector(onSliderEnded), for: [.touchUpInside, .touchUpOutside, .touchCancel])
return s
}()
lazy var progressView: GradientProgressView = {
let v = GradientProgressView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
#objc fileprivate func onSliderChange(_ slider: UISlider) {
let condition: Bool = // ...
let value = slider.value
progressView.setProgress(CGFloat(value), someCondition: condition, slider_X_Position: slider_X_PositionInView())
}
#objc fileprivate func onSliderEnded(_ slider: UISlider) {
let value = slider.value
progressView.resetProgress(CGFloat(value))
}
// ... progressView is the same width as the the slider
func slider_X_PositionInView() -> CGFloat {
let trackRect = slider.trackRect(forBounds: slider.bounds)
let thumbRect = slider.thumbRect(forBounds: slider.bounds,
trackRect: trackRect,
value: slider.value)
let convertedThumbRect = slider.convert(thumbRect, to: self.view)
return convertedThumbRect.midX
}
GradientProgressView:
public class GradientProgressView: UIView {
var shapeLayer: CAShapeLayer = {
// ...
}()
private var trackLayer: CAShapeLayer = {
let trackLayer = CAShapeLayer()
trackLayer.strokeColor = UIColor.black.cgColor
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = .round
return trackLayer
}()
private var gradient: CAGradientLayer = {
let gradient = CAGradientLayer()
let redColor = UIColor.red.cgColor
gradient.colors = [redColor, redColor]
gradient.locations = [0.0, 1.0]
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 0)
return gradient
}()
// ... add the above layers as subLayers to self ...
func updatePaths() { // added in layoutSubviews
let lineWidth = bounds.height / 2
trackLayer.lineWidth = lineWidth * 0.75
shapeLayer.lineWidth = lineWidth
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.minX + lineWidth / 2, y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.maxX - lineWidth / 2, y: bounds.midY))
trackLayer.path = path.cgPath
shapeLayer.path = path.cgPath
gradient.frame = bounds
gradient.mask = shapeLayer
shapeLayer.duration = 1
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = 0
}
public func setProgress(_ progress: CGFloat, someCondition: Bool, slider_X_Position: CGFloat) {
// slider_X_Position might help with shapeLayer's x position for the colors ???
if someCondition {
// redColor until the user lets go
} else {
// otherwise always a clearColor
}
shapeLayer.strokeEnd = progress
}
}
public func resetProgress(_ progress: CGFloat) {
// change to clearColor after finger is lifted
}
}
To get this:
We can use a CAShapeLayer for the red "boxes" and a CALayer as a .mask on that shape layer.
To reveal / cover the boxes, we set the frame of the mask layer to a percentage of the width of the bounds.
Here's a complete example:
class StepView: UIView {
public var progress: CGFloat = 0 {
didSet {
setNeedsLayout()
}
}
public var steps: [[CGFloat]] = [[0.0, 1.0]] {
didSet {
setNeedsLayout()
}
}
public var color: UIColor = .red {
didSet {
stepLayer.fillColor = color.cgColor
}
}
private let stepLayer = CAShapeLayer()
private let maskLayer = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
backgroundColor = .black
layer.addSublayer(stepLayer)
stepLayer.fillColor = color.cgColor
stepLayer.mask = maskLayer
// mask layer can use any solid color
maskLayer.backgroundColor = UIColor.white.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
stepLayer.frame = bounds
let pth = UIBezierPath()
steps.forEach { pair in
// rectangle for each "percentage pair"
let w = bounds.width * (pair[1] - pair[0])
let b = UIBezierPath(rect: CGRect(x: bounds.width * pair[0], y: 0, width: w, height: bounds.height))
pth.append(b)
}
stepLayer.path = pth.cgPath
// update frame of mask layer
var r = bounds
r.size.width = bounds.width * progress
maskLayer.frame = r
}
}
class StepVC: UIViewController {
let stepView = StepView()
override func viewDidLoad() {
super.viewDidLoad()
stepView.translatesAutoresizingMaskIntoConstraints = false
let slider = UISlider()
slider.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stepView)
view.addSubview(slider)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stepView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
stepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stepView.heightAnchor.constraint(equalToConstant: 40.0),
slider.topAnchor.constraint(equalTo: stepView.bottomAnchor, constant: 40.0),
slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
let steps: [[CGFloat]] = [
[0.1, 0.3],
[0.4, 0.5],
[0.7, 0.8],
[0.9, 0.95],
]
stepView.steps = steps
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
}
#objc func sliderChanged(_ sender: UISlider) {
// disable CALayer "built-in" animations
CATransaction.setDisableActions(true)
stepView.progress = CGFloat(sender.value)
CATransaction.commit()
}
}
Edit
I'm still not clear on your 0.4 - 0.8 requirement, but maybe this will help get you on your way:
Please note: this is Example Code Only!!!
struct RecordingStep {
var color: UIColor = .black
var start: Float = 0
var end: Float = 0
var layer: CALayer!
}
class StepView2: UIView {
public var progress: Float = 0 {
didSet {
// move the progress layer
progressLayer.position.x = bounds.width * CGFloat(progress)
// if we're recording
if isRecording {
let i = theSteps.count - 1
guard i > -1 else { return }
// update current "step" end
theSteps[i].end = progress
setNeedsLayout()
}
}
}
private var isRecording: Bool = false
private var theSteps: [RecordingStep] = []
private let progressLayer = CAShapeLayer()
public func startRecording(_ color: UIColor) {
// create a new "Recording Step"
var st = RecordingStep()
st.color = color
st.start = progress
st.end = progress
let l = CALayer()
l.backgroundColor = st.color.cgColor
layer.insertSublayer(l, below: progressLayer)
st.layer = l
theSteps.append(st)
isRecording = true
}
public func stopRecording() {
isRecording = false
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
backgroundColor = .black
progressLayer.lineWidth = 3
progressLayer.strokeColor = UIColor.green.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(progressLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
// only set the progessLayer frame if the bounds height has changed
if progressLayer.frame.height != bounds.height + 7.0 {
let r: CGRect = CGRect(origin: .zero, size: CGSize(width: 7.0, height: bounds.height + 7.0))
let pth = UIBezierPath(roundedRect: r, cornerRadius: 3.5)
progressLayer.frame = r
progressLayer.position = CGPoint(x: 0, y: bounds.midY)
progressLayer.path = pth.cgPath
}
theSteps.forEach { st in
let x = bounds.width * CGFloat(st.start)
let w = bounds.width * CGFloat(st.end - st.start)
let r = CGRect(x: x, y: 0.0, width: w, height: bounds.height)
st.layer.frame = r
}
}
}
class Step2VC: UIViewController {
let stepView = StepView2()
let actionButton: UIButton = {
let b = UIButton()
b.backgroundColor = .lightGray
b.setImage(UIImage(systemName: "play.fill"), for: [])
b.tintColor = .systemGreen
return b
}()
var timer: Timer!
let colors: [UIColor] = [
.red, .systemBlue, .yellow, .cyan, .magenta, .orange,
]
var colorIdx: Int = -1
var action: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
stepView.translatesAutoresizingMaskIntoConstraints = false
actionButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stepView)
view.addSubview(actionButton)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stepView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
stepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stepView.heightAnchor.constraint(equalToConstant: 40.0),
actionButton.topAnchor.constraint(equalTo: stepView.bottomAnchor, constant: 40.0),
actionButton.widthAnchor.constraint(equalToConstant: 80.0),
actionButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
actionButton.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
}
#objc func timerFunc(_ timer: Timer) {
// don't set progress > 1.0
stepView.progress = min(stepView.progress + 0.005, 1.0)
if stepView.progress >= 1.0 {
timer.invalidate()
actionButton.isHidden = true
}
}
#objc func btnTap(_ sender: UIButton) {
switch action {
case 0:
// this will run for 15 seconds
timer = Timer.scheduledTimer(timeInterval: 0.075, target: self, selector: #selector(timerFunc(_:)), userInfo: nil, repeats: true)
stepView.stopRecording()
actionButton.setImage(UIImage(systemName: "record.circle"), for: [])
actionButton.tintColor = .red
action = 1
case 1:
colorIdx += 1
stepView.startRecording(colors[colorIdx % colors.count])
actionButton.setImage(UIImage(systemName: "stop.circle"), for: [])
actionButton.tintColor = .black
action = 2
case 2:
stepView.stopRecording()
actionButton.setImage(UIImage(systemName: "record.circle"), for: [])
actionButton.tintColor = .red
action = 1
default:
()
}
}
}
For future reference, when posting here, it's probably a good idea to fully explain what you're trying to do. Showing code you're working on is important, but if it's really only sorta related to your actual goal, it makes this process pretty difficult.

Animation not happening as it is supposed to be for subclass of UIView with layers

I am trying to animate a subclass of UIView that has a some layers with shadow. In a view controller, I set this view inside a container. I animate container's height constraint. The container (the purple one in the video) animates properly, but the view that is supposed to be animated, doesn't animate the way it should be.
How it looks now
This is how I animate the container view.
func updateWhiteCircle(with progressHeight: CGFloat?) {
guard let progressHeight = progressHeight else {
return
}
neumorphicRingProgressHeightConstraint.constant = progressHeight
UIView.animate(withDuration: 1.0) { [weak self] in
self?.view.layoutIfNeeded()
}
}
Where do I get it wrong? Why doesn't it animate the way it should be?
You didn't show us how you're generating the "white shadow," but likely you're setting it in the view's layoutSubviews() func.
The problem is, that does not animate with the size of the view.
You probably want to animate the path for the "shadow layer."
Here's a quick example:
class WhiteCircleView: UIView {
private let shapeLayer = CAShapeLayer()
// public vars so we can set various properties
public var fillColor: UIColor = .white {
didSet {
shapeLayer.fillColor = fillColor.cgColor
}
}
public var shadowColor: UIColor = .white {
didSet {
shapeLayer.shadowColor = shadowColor.cgColor
}
}
public var shadowOpacity: Float = 1.0 {
didSet {
shapeLayer.shadowOpacity = shadowOpacity
}
}
public var shadowRadius: CGFloat = 20 {
didSet {
shapeLayer.shadowRadius = shadowRadius
}
}
public var shadowOffset: CGSize = .zero {
didSet {
shapeLayer.shadowOffset = shadowOffset
}
}
public var progress: CGFloat = 0 {
didSet {
animCircle()
}
}
private var curProgress: CGFloat = 0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
shapeLayer.fillColor = fillColor.cgColor
shapeLayer.shadowColor = shadowColor.cgColor
shapeLayer.shadowOffset = shadowOffset
shapeLayer.shadowRadius = shadowRadius
shapeLayer.shadowOpacity = shadowOpacity
layer.addSublayer(shapeLayer)
}
override func layoutSubviews() {
let w: CGFloat = bounds.width * CGFloat(progress)
let wi: CGFloat = (bounds.width - w) * 0.5
let newPath = UIBezierPath(ovalIn: bounds.insetBy(dx: wi, dy: wi)).cgPath
shapeLayer.path = newPath
}
private func animCircle() {
print(progress)
let w: CGFloat = bounds.width * CGFloat(progress)
let wi: CGFloat = (bounds.width - w) * 0.5
let curPath = shapeLayer.path
let newPath = UIBezierPath(ovalIn: bounds.insetBy(dx: wi, dy: wi)).cgPath
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = curPath
animation.toValue = newPath
animation.duration = 0.5
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
CATransaction.setCompletionBlock({
// update to new path on anim end
self.shapeLayer.path = newPath
})
shapeLayer.add(animation, forKey: "grow")
CATransaction.commit()
}
}
class AnimCircleVC: UIViewController {
let wcv = WhiteCircleView()
let bkgColor: UIColor = UIColor(white: 0.75, alpha: 1.0)
var progress: CGFloat = 0
// a "center label" to show progress value
let pLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .systemYellow
v.textAlignment = .center
v.text = "0%"
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = bkgColor
wcv.fillColor = bkgColor
wcv.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(wcv)
pLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
wcv.centerXAnchor.constraint(equalTo: g.centerXAnchor),
wcv.centerYAnchor.constraint(equalTo: g.topAnchor, constant: 200.0),
wcv.heightAnchor.constraint(equalToConstant: 200.0),
wcv.widthAnchor.constraint(equalTo: wcv.heightAnchor),
pLabel.centerXAnchor.constraint(equalTo: wcv.centerXAnchor),
pLabel.centerYAnchor.constraint(equalTo: wcv.centerYAnchor),
pLabel.widthAnchor.constraint(equalToConstant: 100.0),
pLabel.heightAnchor.constraint(equalTo: pLabel.widthAnchor),
])
pLabel.layer.cornerRadius = 50.0
pLabel.layer.masksToBounds = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if progress >= 1.0 {
// shrink it away
progress = 0.0
} else {
if progress == 0.0 {
// start at 60%
progress = 0.6
} else {
// increment by 10%
progress += 0.10
}
}
// floating point can result in 0.99999...
// so round to 1/100th
progress = (progress * 100).rounded() / 100.0
wcv.progress = progress
pLabel.text = "\(Int(progress * 100))%"
}
}

Animate UIView's Layer with constrains (Auto Layout Animations)

I am working on project where I need to animate height of view which consist of shadow, gradient and rounded corners (not all corners).
So I have used layerClass property of view.
Below is simple example demonstration.
Problem is that, when I change height of view by modifying its constraint, it was resulting in immediate animation of layer class, which is kind of awkward.
Below is my sample code
import UIKit
class CustomView: UIView{
var isAnimating: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
func setupView(){
self.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
guard let layer = self.layer as? CAShapeLayer else { return }
layer.fillColor = UIColor.green.cgColor
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
layer.shadowOpacity = 0.6
layer.shadowRadius = 5
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override func layoutSubviews() {
super.layoutSubviews()
// While animating `myView` height, this method gets called
// So new bounds for layer will be calculated and set immediately
// This result in not proper animation
// check by making below condition always true
if !self.isAnimating{ //if true{
guard let layer = self.layer as? CAShapeLayer else { return }
layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
layer.shadowPath = layer.path
}
}
}
class TestViewController : UIViewController {
// height constraint for animating height
var heightConstarint: NSLayoutConstraint?
var heightToAnimate: CGFloat = 200
lazy var myView: CustomView = {
let view = CustomView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .clear
return view
}()
lazy var mySubview: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .yellow
return view
}()
lazy var button: UIButton = {
let button = UIButton(frame: .zero)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Animate", for: .normal)
button.setTitleColor(.black, for: .normal)
button.addTarget(self, action: #selector(self.animateView(_:)), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(self.myView)
self.myView.addSubview(self.mySubview)
self.view.addSubview(self.button)
self.myView.leadingAnchor.constraint(equalTo: self.view.layoutMarginsGuide.leadingAnchor).isActive = true
self.myView.trailingAnchor.constraint(equalTo: self.view.layoutMarginsGuide.trailingAnchor).isActive = true
self.myView.topAnchor.constraint(equalTo: self.view.layoutMarginsGuide.topAnchor, constant: 100).isActive = true
self.heightConstarint = self.myView.heightAnchor.constraint(equalToConstant: 100)
self.heightConstarint?.isActive = true
self.mySubview.leadingAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.leadingAnchor).isActive = true
self.mySubview.trailingAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.trailingAnchor).isActive = true
self.mySubview.topAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.topAnchor).isActive = true
self.mySubview.bottomAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.bottomAnchor).isActive = true
self.button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
self.button.bottomAnchor.constraint(equalTo: self.view.layoutMarginsGuide.bottomAnchor, constant: -20).isActive = true
}
#objc func animateView(_ sender: UIButton){
CATransaction.begin()
CATransaction.setAnimationDuration(5.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))
UIView.animate(withDuration: 5.0, animations: {
self.myView.isAnimating = true
self.heightConstarint?.constant = self.heightToAnimate
// this will call `myView.layoutSubviews()`
// and layer's new bound will set automatically
// this causing layer to be directly jump to height 200, instead of smooth animation
self.view.layoutIfNeeded()
}) { (success) in
self.myView.isAnimating = false
}
let shadowPathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.shadowPath))
let pathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path))
let toValue = UIBezierPath(
roundedRect:CGRect(x: 0, y: 0, width: self.myView.bounds.width, height: heightToAnimate),
cornerRadius: 10
).cgPath
shadowPathAnimation.fromValue = self.myView.layer.shadowPath
shadowPathAnimation.toValue = toValue
pathAnimation.fromValue = (self.myView.layer as! CAShapeLayer).path
pathAnimation.toValue = toValue
self.myView.layer.shadowPath = toValue
(self.myView.layer as! CAShapeLayer).path = toValue
self.myView.layer.add(shadowPathAnimation, forKey: #keyPath(CAShapeLayer.shadowPath))
self.myView.layer.add(pathAnimation, forKey: #keyPath(CAShapeLayer.path))
CATransaction.commit()
}
}
While animating view, it will call its layoutSubviews() method, which will result into recalculating bounds of shadow layer.
So I checked if view is currently animating, then do not recalculate bounds of shadow layer.
Is this approach right ? or there is any better way to do this ?
I know it's a tricky question. Actually, you don't need to care about layoutSubViews at all. The key here is when you set the shapeLayer. If it's setup well, i.e. after the constraints are all working, you don't need to care that during the animation.
//in CustomView, comment out the layoutSubViews() and add updateLayer()
func updateLayer(){
guard let layer = self.layer as? CAShapeLayer else { return }
layer.path = UIBezierPath(roundedRect: layer.bounds, cornerRadius: 10).cgPath
layer.shadowPath = layer.path
}
// override func layoutSubviews() {
// super.layoutSubviews()
//
// // While animating `myView` height, this method gets called
// // So new bounds for layer will be calculated and set immediately
// // This result in not proper animation
//
// // check by making below condition always true
//
// if !self.isAnimating{ //if true{
// guard let layer = self.layer as? CAShapeLayer else { return }
//
// layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
// layer.shadowPath = layer.path
// }
// }
in ViewController: add viewDidAppear() and remove other is animation block
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myView.updateLayer()
}
#objc func animateView(_ sender: UIButton){
CATransaction.begin()
CATransaction.setAnimationDuration(5.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))
UIView.animate(withDuration: 5.0, animations: {
self.heightConstarint?.constant = self.heightToAnimate
// this will call `myView.layoutSubviews()`
// and layer's new bound will set automatically
// this causing layer to be directly jump to height 200, instead of smooth animation
self.view.layoutIfNeeded()
}) { (success) in
self.myView.isAnimating = false
}
....
Then you are good to go. Have a wonderful day.
Below code also worked for me, As I want to use layout subviews without any flags.
UIView.animate(withDuration: 5.0, animations: {
let shadowPathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.shadowPath))
let pathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path))
let toValue = UIBezierPath(
roundedRect:CGRect(x: 0, y: 0, width: self.myView.bounds.width, height: self.heightToAnimate),
cornerRadius: 10
).cgPath
shadowPathAnimation.fromValue = self.myView.layer.shadowPath
shadowPathAnimation.toValue = toValue
pathAnimation.fromValue = (self.myView.layer as! CAShapeLayer).path
pathAnimation.toValue = toValue
self.heightConstarint?.constant = self.heightToAnimate
self.myView.layoutIfNeeded()
self.myView.layer.shadowPath = toValue
(self.myView.layer as! CAShapeLayer).path = toValue
self.myView.layer.add(shadowPathAnimation, forKey: #keyPath(CAShapeLayer.shadowPath))
self.myView.layer.add(pathAnimation, forKey: #keyPath(CAShapeLayer.path))
CATransaction.commit()
})
And overriding layoutSubview as follows
override func layoutSubviews() {
super.layoutSubviews()
guard let layer = self.layer as? CAShapeLayer else { return }
layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
layer.shadowPath = layer.path
}

IOS Step menu using swift

I would like to create the stepper menu in IOS using swift, But I'm facing some issues. Here are the issues.
1) Portrait and landscape stepper menu is not propper.
2) How to set default step position with the method below method, It's working when button clicked. But I want to set when menu loads the first time.
self.stepView.setSelectedPosition(index: 2)
3) If it reached the position last, I would like to change the color for complete path parentPathRect.
4) Progress animation CABasicAnimation is not like the progress bar, I want to show the animation.
5) It should not remove the selected position color when changing the orientation.
As per my organization rules should not use third-party frameworks.
Can anyone help me with the solution? Or is there any alternative solution for this?
Here is my code:
import UIKit
class ViewController: UIViewController, StepMenuDelegate {
#IBOutlet weak var stepView: StepView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.stepView.delegate = self;
self.stepView.titles = ["1", "2", "3"]
self.stepView.lineWidth = 8
self.stepView.offSet = 8
self.stepView.setSelectedPosition(index: 2)
}
func didSelectItem(atIndex index: NSInteger) {
print(index)
}
}
protocol StepMenuDelegate {
func didSelectItem(atIndex index: NSInteger)
}
class StepView: UIView {
var delegate : StepMenuDelegate!
var titles: [String] = [] {
didSet(values) {
setup()
setupItems()
}
}
var lineWidth: CGFloat = 8 {
didSet(values) {
setup()
}
}
var offSet: CGFloat = 8 {
didSet(values) {
self.itemOffset = offSet * 4
setup()
}
}
private var selectedIndex : NSInteger!
private var itemOffset : CGFloat = 8 {
didSet (value) {
setup()
setupItems()
}
}
private var path : UIBezierPath!
var selectedLayer : CAShapeLayer!
private var parentPathRect : CGRect!
override func awakeFromNib() {
super.awakeFromNib()
}
override func layoutSubviews() {
self.setup()
setupItems()
}
func setup() {
self.removeAllButtonsAndLayes()
let layer = CAShapeLayer()
self.parentPathRect = CGRect(origin: CGPoint(x: offSet, y: self.bounds.midY - (self.lineWidth/2) ), size: CGSize(width: self.bounds.width - (offSet * 2), height: lineWidth))
path = UIBezierPath(roundedRect: self.parentPathRect, cornerRadius: 2)
layer.path = path.cgPath
layer.fillColor = UIColor.orange.cgColor
layer.lineCap = .butt
layer.shadowColor = UIColor.darkGray.cgColor
layer.shadowOffset = CGSize(width: 1, height: 2)
layer.shadowOpacity = 0.1
layer.shadowRadius = 2
self.layer.addSublayer(layer)
}
func setupItems() {
removeAllButtonsAndLayes()
let itemRect = CGRect(x: self.itemOffset, y: 0, width: 34, height: 34)
let totalWidth = self.bounds.width
let itemWidth = totalWidth / CGFloat(self.titles.count);
for i in 0..<self.titles.count {
let button = UIButton()
var xPos: CGFloat = itemOffset
self.addSubview(button)
xPos += (CGFloat(i) * itemWidth);
xPos += itemOffset/3
button.translatesAutoresizingMaskIntoConstraints = false
button.leftAnchor.constraint(equalTo: self.leftAnchor, constant: xPos).isActive = true
button.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 0).isActive = true
button.heightAnchor.constraint(equalToConstant: itemRect.height).isActive = true
button.widthAnchor.constraint(equalToConstant: itemRect.width).isActive = true
button.backgroundColor = UIColor.red
button.layer.zPosition = 1
button.layer.cornerRadius = itemRect.height/2
let name : String = self.titles[i]
button.tag = i
button.setTitle(name, for: .normal)
button.addTarget(self, action: #selector(selectedItemEvent(sender:)), for: .touchUpInside)
if self.selectedIndex != nil {
if button.tag == self.selectedIndex {
selectedItemEvent(sender: button)
}
}
}
}
#objc func selectedItemEvent(sender:UIButton) {
if self.selectedLayer != nil {
selectedLayer.removeFromSuperlayer()
}
self.delegate.didSelectItem(atIndex: sender.tag)
let fromRect = self.parentPathRect.origin
self.selectedLayer = CAShapeLayer()
let rect = CGRect(origin: fromRect, size: CGSize(width:sender.frame.origin.x - 4, height: 8))
let path = UIBezierPath(roundedRect: rect, cornerRadius: 4)
self.selectedLayer.path = path.cgPath
self.selectedLayer.lineCap = .round
self.selectedLayer.fillColor = UIColor.orange.cgColor
let animation = CABasicAnimation(keyPath: "fillColor")
animation.toValue = UIColor.blue.cgColor
animation.duration = 0.2
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
self.selectedLayer.add(animation, forKey: "fillColor")
self.layer.addSublayer(self.selectedLayer)
}
func removeAllButtonsAndLayes() {
for button in self.subviews {
if button is UIButton {
button.removeFromSuperview()
}
}
}
func setSelectedPosition(index:NSInteger) {
self.selectedIndex = index
}
}
Here I found One way to achieve the solution.!!
https://gist.github.com/DamodarGit/7f0f484708f60c996772ae28e5e1c615
Welcome to suggestions or code changes.!!

Activity indicator with custom image

I am loading a UIWebView and in the meantime I wan't to show a blank page with this activity indicator spinning (siri activity indicator). From what I have understand you can not change the image, but can't I use that image and create an animation with it rotating 360° and looping? Or will that drain the battery?
something like this?:
- (void)webViewDidStartLoad:(UIWebView *)webView {
//set up animation
[self.view addSubview:self.loadingImage];
//start animation
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
//stop animation
[self.loadingImage removeFromSuperview];
}
What should I do?
Thanks in advance!
Most of this is found in Stack Overflow. Let me summarize:
Create an UIImageView which will serve as an activity indicator (inside storyboard scene, NIB, code ... wherever you wish). Let's call it _activityIndicatorImage
Load your image: _activityIndicatorImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"activity_indicator"]];
You need to use animation to rotate it. Here is the method I use:
+ (void)rotateLayerInfinite:(CALayer *)layer
{
CABasicAnimation *rotation;
rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotation.fromValue = [NSNumber numberWithFloat:0];
rotation.toValue = [NSNumber numberWithFloat:(2 * M_PI)];
rotation.duration = 0.7f; // Speed
rotation.repeatCount = HUGE_VALF; // Repeat forever. Can be a finite number.
[layer removeAllAnimations];
[layer addAnimation:rotation forKey:#"Spin"];
}
Inside my layoutSubviews method I initiate rotation. You could place this in your webViewDidStartLoad and webViewDidFinishLoad if this is better for your case:
- (void)layoutSubviews
{
[super layoutSubviews];
// some other code
[Utils rotateLayerInfinite:_activityIndicatorImage.layer];
}
You could always always stop rotation using [_activityIndicatorImage.layer removeAllAnimations];
You may use this beautiful loader inspired from Tumblr app:
Asich/AMTumblrHud
Swift 5
Another answer working perfect
Step 1.
Create swift file "CustomLoader.swift" and put this code in that file
import UIKit
import CoreGraphics
import QuartzCore
class CustomLoader: UIView
{
//MARK:- NOT ACCESSABLE OUT SIDE
fileprivate var duration : CFTimeInterval! = 1
fileprivate var isAnimating :Bool = false
fileprivate var backgroundView : UIView!
//MARK:- ACCESS INSTANCE ONLY AND CHANGE ACCORDING TO YOUR NEEDS *******
let colors : [UIColor] = [.red, .blue, .orange, .purple]
var defaultColor : UIColor = UIColor.red
var isUsrInteractionEnable : Bool = false
var defaultbgColor: UIColor = UIColor.white
var loaderSize : CGFloat = 80.0
/// **************** ****************** ////////// **************
//MARK:- MAKE SHARED INSTANCE
private static var Instance : CustomLoader!
static let sharedInstance : CustomLoader = {
if Instance == nil
{
Instance = CustomLoader()
}
return Instance
}()
//MARK:- DESTROY TO SHARED INSTANCE
#objc fileprivate func destroyShardInstance()
{
CustomLoader.Instance = nil
}
//MARK:- SET YOUR LOADER INITIALIZER FRAME ELSE DEFAULT IS CENTER
func startAnimation()
{
let win = UIApplication.shared.keyWindow
backgroundView = UIView()
backgroundView.frame = (UIApplication.shared.keyWindow?.frame)!
backgroundView.backgroundColor = UIColor.init(white: 0, alpha: 0.4)
win?.addSubview(backgroundView)
self.frame = CGRect.init(x: ((UIScreen.main.bounds.width) - loaderSize)/2, y: ((UIScreen.main.bounds.height) - loaderSize)/2, width: loaderSize, height: loaderSize)
self.addCenterImage()
self.isHidden = false
self.backgroundView.addSubview(self)
self.layer.cornerRadius = loaderSize/2
self.layer.masksToBounds = true
backgroundView.accessibilityIdentifier = "CustomLoader"
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.NSExtensionHostDidBecomeActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(CustomLoader.ResumeLoader), name: NSNotification.Name.NSExtensionHostDidBecomeActive, object: nil)
self.layoutSubviews()
}
//MARK:- AVOID STUCKING LOADER WHEN CAME BACK FROM BACKGROUND
#objc fileprivate func ResumeLoader()
{
if isAnimating
{
self.stopAnimation()
self.AnimationStart()
}
}
override func layoutSubviews()
{
super.layoutSubviews()
self.backgroundColor = defaultbgColor
UIApplication.shared.keyWindow?.isUserInteractionEnabled = isUsrInteractionEnable
self.AnimationStart()
}
#objc fileprivate func addCenterImage()
{
/// add image in center
let centerImage = UIImage(named: "Logo")
let imageSize = loaderSize/2.5
let centerImgView = UIImageView(image: centerImage)
centerImgView.frame = CGRect(
x: (self.bounds.width - imageSize) / 2 ,
y: (self.bounds.height - imageSize) / 2,
width: imageSize,
height: imageSize
)
centerImgView.contentMode = .scaleAspectFit
centerImgView.layer.cornerRadius = imageSize/2
centerImgView.clipsToBounds = true
self.addSubview(centerImgView)
}
//MARK:- CALL IT TO START THE LOADER , AFTER INITIALIZE THE LOADER
#objc fileprivate func AnimationStart()
{
if isAnimating
{
return
}
let size = CGSize.init(width: loaderSize , height: loaderSize)
let dotNum: CGFloat = 10
let diameter: CGFloat = size.width / 5.5 //10
let dot = CALayer()
let frame = CGRect(
x: (layer.bounds.width - diameter) / 2 + diameter * 2,
y: (layer.bounds.height - diameter) / 2,
width: diameter/1.3,
height: diameter/1.3
)
dot.backgroundColor = colors[0].cgColor
dot.cornerRadius = frame.width / 2
dot.frame = frame
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame = layer.bounds
replicatorLayer.instanceCount = Int(dotNum)
replicatorLayer.instanceDelay = 0.1
let angle = (2.0 * M_PI) / Double(replicatorLayer.instanceCount)
replicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)
layer.addSublayer(replicatorLayer)
replicatorLayer.addSublayer(dot)
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.toValue = 0.4
scaleAnimation.duration = 0.5
scaleAnimation.autoreverses = true
scaleAnimation.repeatCount = .infinity
scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
dot.add(scaleAnimation, forKey: "scaleAnimation")
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.toValue = -2.0 * Double.pi
rotationAnimation.duration = 6.0
rotationAnimation.repeatCount = .infinity
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
replicatorLayer.add(rotationAnimation, forKey: "rotationAnimation")
if colors.count > 1 {
var cgColors : [CGColor] = []
for color in colors {
cgColors.append(color.cgColor)
}
let colorAnimation = CAKeyframeAnimation(keyPath: "backgroundColor")
colorAnimation.values = cgColors
colorAnimation.duration = 2
colorAnimation.repeatCount = .infinity
colorAnimation.autoreverses = true
dot.add(colorAnimation, forKey: "colorAnimation")
}
self.isAnimating = true
self.isHidden = false
}
//MARK:- CALL IT TO STOP THE LOADER
func stopAnimation()
{
if !isAnimating
{
return
}
UIApplication.shared.keyWindow?.isUserInteractionEnabled = true
let winSubviews = UIApplication.shared.keyWindow?.subviews
if (winSubviews?.count)! > 0
{
for viw in winSubviews!
{
if viw.accessibilityIdentifier == "CustomLoader"
{
viw.removeFromSuperview()
// break
}
}
}
layer.sublayers = nil
isAnimating = false
self.isHidden = true
self.destroyShardInstance()
}
//MARK:- GETTING RANDOM COLOR , AND MANAGE YOUR OWN COLORS
#objc fileprivate func randomColor()->UIColor
{
let randomRed:CGFloat = CGFloat(drand48())
let randomGreen:CGFloat = CGFloat(drand48())
let randomBlue:CGFloat = CGFloat(drand48())
return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: 1.0)
}
override func draw(_ rect: CGRect)
{
}
}
find the func name and "addCenterImage" and replace the image name with your custom image.
Step 2
Create the AppDelegate class instance out side of the AppDelegate class like this.
var AppInstance: AppDelegate!
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
{ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
AppInstance = self
}
Step 3.
put these two func in your AppDelegate
//MARK: - Activity Indicator -
func showLoader()
{
CustomLoader.sharedInstance.startAnimation()
}
func hideLoader()
{
CustomLoader.sharedInstance.stopAnimation()
}
Step 4. Use the functions like this whenever you want to animate your loader and stop.
AppInstance.showLoader()
AppInstance.hideLoader()
HAPPY LOADING...
SWIFT 4 Sweet And Simply just put extension UIView{}
Modified answer of #gandhi Mena
if you want to create your own custom Loading indicator
Create a UIView extension which create and customize your brand logo as a custom indicator put this code in you global declaration file.
extension UIView{
func customActivityIndicator(view: UIView, widthView: CGFloat?,backgroundColor: UIColor?, textColor:UIColor?, message: String?) -> UIView{
//Config UIView
self.backgroundColor = backgroundColor //Background color of your view which you want to set
var selfWidth = view.frame.width
if widthView != nil{
selfWidth = widthView ?? selfWidth
}
let selfHeigh = view.frame.height
let loopImages = UIImageView()
let imageListArray = ["image1", "image2"] // Put your desired array of images in a specific order the way you want to display animation.
loopImages.animationImages = imageListArray
loopImages.animationDuration = TimeInterval(0.8)
loopImages.startAnimating()
let imageFrameX = (selfWidth / 2) - 30
let imageFrameY = (selfHeigh / 2) - 60
var imageWidth = CGFloat(60)
var imageHeight = CGFloat(60)
if widthView != nil{
imageWidth = widthView ?? imageWidth
imageHeight = widthView ?? imageHeight
}
//ConfigureLabel
let label = UILabel()
label.textAlignment = .center
label.textColor = .gray
label.font = UIFont(name: "SFUIDisplay-Regular", size: 17.0)! // Your Desired UIFont Style and Size
label.numberOfLines = 0
label.text = message ?? ""
label.textColor = textColor ?? UIColor.clear
//Config frame of label
let labelFrameX = (selfWidth / 2) - 100
let labelFrameY = (selfHeigh / 2) - 10
let labelWidth = CGFloat(200)
let labelHeight = CGFloat(70)
// Define UIView frame
self.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height)
//ImageFrame
loopImages.frame = CGRect(x: imageFrameX, y: imageFrameY, width: imageWidth, height: imageHeight)
//LabelFrame
label.frame = CGRect(x: labelFrameX, y: labelFrameY, width: labelWidth, height: labelHeight)
//add loading and label to customView
self.addSubview(loopImages)
self.addSubview(label)
return self }}
Hide an indicator something like this you can remove subview at the top from the subview stack. put this code in the same globally declared swift file.
func hideLoader(removeFrom : UIView){
removeFrom.subviews.last?.removeFromSuperview()
}
Now you can shoot at the mark by this code
To display activity indicator in your view controller put this code when you want to display.
self.view.addSubview(UIView().customActivityIndicator(view: self.view, widthView: nil, backgroundColor:"Desired color", textColor: "Desired color", message: "Loading something"))
To hide animating loader you can user above function you defined in the globally. In your ViewController.swift where you want to hide put this line of code.
hideLoader(removeFrom: self.view)
imageListArray looks like this.
I've faced a similar issue lately. And this is my solution. Basically, it's what topic starter initially wanted: blank page with custom activity indicator on it.
I have partly used #Azharhussain Shaikh answer but I've implemented auto-layout instead of using frames and added a few other refinements with the intention to make usage as simple as possible.
So, it's an extension for UIView with two methods: addActivityIndicator() and removeActivityIndicator()
extension UIView {
func addActivityIndicator() {
// creating a view (let's call it "loading" view) which will be added on top of the view you want to have activity indicator on (parent view)
let view = UIView()
// setting up a background for a view so it would make content under it look like not active
view.backgroundColor = UIColor.white.withAlphaComponent(0.7)
// adding "loading" view to a parent view
// setting up auto-layout anchors so it would cover whole parent view
self.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
view.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
view.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// creating array with images, which will be animated
// in my case I have 30 images with names activity0.png ... activity29.png
var imagesArray = [UIImage(named: "activity\(0)")!]
for i in 1..<30 {
imagesArray.append(UIImage(named: "activity\(i)")!)
}
// creating UIImageView with array of images
// setting up animation duration and starting animation
let activityImage = UIImageView()
activityImage.animationImages = imagesArray
activityImage.animationDuration = TimeInterval(0.7)
activityImage.startAnimating()
// adding UIImageView on "loading" view
// setting up auto-layout anchors so it would be in center of "loading" view with 30x30 size
view.addSubview(activityImage)
activityImage.translatesAutoresizingMaskIntoConstraints = false
activityImage.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
activityImage.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
activityImage.widthAnchor.constraint(equalToConstant: 30).isActive = true
activityImage.heightAnchor.constraint(equalToConstant: 30).isActive = true
}
func removeActivityIndicator() {
// checking if a view has subviews on it
guard let lastSubView = self.subviews.last else { return }
// removing last subview with an assumption that last view is a "loading" view
lastSubView.removeFromSuperview()
} }
"Rotating" effect is achieved by those 30 images you've put in imagesArray. Each image is a new frame of a rotating indicator like this.
Usage. In your view controller for showing an activity indicator simply put:
view.addActivityIndicator()
For removing an activity indicator:
view.removeActivityIndicator()
For example, in case of using it with table view (like I do) it can be used like this:
func setLoadingScreen() {
view.addActivityIndicator()
tableView.isScrollEnabled = false
}
func removeLoadingScreen() {
view.removeActivityIndicator()
tableView.isScrollEnabled = true
}
It works in Swift 4.
Swift 5.0 version of accepted Answer
public extension UIImageView {
func spin(duration: Float) {
let rotation = CABasicAnimation(keyPath: "transform.rotation")
rotation.fromValue = 0
rotation.toValue = 2 * Double.pi
rotation.duration = 0.7
rotation.repeatCount = duration
layer.add(rotation, forKey: "spin")
}
func stopSpinning() {
layer.removeAllAnimations()
}
}
Without Image , you can use third party library
for objective C (also support in iOS 6) https://github.com/shebinkoshy/UIControllsRepo
for swift https://github.com/shebinkoshy/Activity-Indicator-Swift
Advantages
-> Able to set colors for spinner
-> Available in different sizes like tiny, small, medium, large, very large
-> Able to set Title (center and bottom) for medium, large, very large sizes
You can set an images to your activityIndicator. I created a function for add custom image to activityIndicator. Here is what I created.
public func showProgressView(view: UIView) -> UIImageView {
let containerView = UIView()
let progressView = UIView()
var activityIndicatorImageView = UIImageView()
if let statusImage = UIImage(named: Constants.ActivityIndicatorImageName1) {
let activityImageView = UIImageView(image: statusImage)
containerView.frame = view.frame
containerView.backgroundColor = UIColor(hex: 0xffffff, alpha: 0.3)
progressView.frame = CGRectMake(0, 0, 80, 80)
progressView.center = CGPointMake(view.bounds.width / 2, view.bounds.height / 2)
progressView.backgroundColor = UIColor(hex: 0x18bda3, alpha: 0.7)
progressView.clipsToBounds = true
progressView.layer.cornerRadius = 10
activityImageView.animationImages = [UIImage(named: Constants.ActivityIndicatorImageName1)!,
UIImage(named: Constants.ActivityIndicatorImageName2)!,
UIImage(named: Constants.ActivityIndicatorImageName3)!,
UIImage(named: Constants.ActivityIndicatorImageName4)!,
UIImage(named: Constants.ActivityIndicatorImageName5)!]
activityImageView.animationDuration = 0.8;
activityImageView.frame = CGRectMake(view.frame.size.width / 2 - statusImage.size.width / 2, view.frame.size.height / 2 - statusImage.size.height / 2, 40.0, 48.0)
activityImageView.center = CGPointMake(progressView.bounds.width / 2, progressView.bounds.height / 2)
dispatch_async(dispatch_get_main_queue()) {
progressView.addSubview(activityImageView)
containerView.addSubview(progressView)
view.addSubview(containerView)
activityIndicatorImageView = activityImageView
}
}
return activityIndicatorImageView
}
You can call this method everywhere in your code. And just call the startAnimating method. If you want to hide just call the stopAnimating method.
it works in both SWITF 3 and 4
var activityIndicator = UIActivityIndicatorView()
var myView : UIView = UIView()
func viewDidLoad() {
spinnerCreation()
}
func spinnerCreation() {
activityIndicator.activityIndicatorViewStyle = .whiteLarge
let label = UILabel.init(frame: CGRect(x: 5, y: 60, width: 90, height: 20))
label.textColor = UIColor.white
label.font = UIFont.boldSystemFont(ofSize: 14.0)
label.textAlignment = NSTextAlignment.center
label.text = "Please wait...."
myView.frame = CGRect(x: (UIScreen.main.bounds.size.width - 100)/2, y: (UIScreen.main.bounds.size.height - 100)/2, width: 100, height: 100)
myView.backgroundColor = UIColor.init(white: 0.0, alpha: 0.7)
myView.layer.cornerRadius = 5
activityIndicator.center = CGPoint(x: myView.frame.size.width/2, y: myView.frame.size.height/2 - 10)
myView.addSubview(activityIndicator)
myView.addSubview(label)
myView.isHidden = true
self.window?.addSubview(myView)
}
#IBAction func activityIndicatorStart(_ sender: Any) {
myView.isHidden = false
self.activityIndicator.startAnimating()
self.view.isUserInteractionEnabled = false
self.view.bringSubview(toFront: myView)
}
#IBAction func activityIndicatorStop(_ sender: Any)() {
myView.isHidden = true
self.activityIndicator.stopAnimating()
self.view.isUserInteractionEnabled = true
}
You can create your custom activity Indicator with this in Swift 3 & 4:
Create a new file with name: UIViewExtension.Swift and copy this code and paste in your new file file:
import UIkit
extension UIView{
func customActivityIndicator(view: UIView, widthView: CGFloat? = nil,backgroundColor: UIColor? = nil, message: String? = nil,colorMessage:UIColor? = nil ) -> UIView{
//Config UIView
self.backgroundColor = backgroundColor ?? UIColor.clear
self.layer.cornerRadius = 10
var selfWidth = view.frame.width - 100
if widthView != nil{
selfWidth = widthView ?? selfWidth
}
let selfHeigh = CGFloat(100)
let selfFrameX = (view.frame.width / 2) - (selfWidth / 2)
let selfFrameY = (view.frame.height / 2) - (selfHeigh / 2)
let loopImages = UIImageView()
//ConfigCustomLoading with secuence images
let imageListArray = [UIImage(named:""),UIImage(named:""), UIImage(named:"")]
loopImages.animationImages = imageListArray
loopImages.animationDuration = TimeInterval(1.3)
loopImages.startAnimating()
let imageFrameX = (selfWidth / 2) - 17
let imageFrameY = (selfHeigh / 2) - 35
var imageWidth = CGFloat(35)
var imageHeight = CGFloat(35)
if widthView != nil{
imageWidth = widthView ?? imageWidth
imageHeight = widthView ?? imageHeight
}
//ConfigureLabel
let label = UILabel()
label.textAlignment = .center
label.textColor = .gray
label.font = UIFont.boldSystemFont(ofSize: 17)
label.numberOfLines = 0
label.text = message ?? ""
label.textColor = colorMessage ?? UIColor.clear
//Config frame of label
let labelFrameX = (selfWidth / 2) - 100
let labelFrameY = (selfHeigh / 2) - 10
let labelWidth = CGFloat(200)
let labelHeight = CGFloat(70)
//add loading and label to customView
self.addSubview(loopImages)
self.addSubview(label)
//Define frames
//UIViewFrame
self.frame = CGRect(x: selfFrameX, y: selfFrameY, width: selfWidth , height: selfHeigh)
//ImageFrame
loopImages.frame = CGRect(x: imageFrameX, y: imageFrameY, width: imageWidth, height: imageHeight)
//LabelFrame
label.frame = CGRect(x: labelFrameX, y: labelFrameY, width: labelWidth, height: labelHeight)
return self
}
}
And then you can use it in your ViewController like this:
import UIKit
class ExampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(UIView().customActivityIndicator(view: self.view,backgroundColor: UIColor.green))
}
//function for stop and desappear loading
func deseappearLoading(){
self.view.subviews.last?.removeFromSuperview()
}
}
Don't forget replace [UIImage(named:" "),UIImage(named:" "), UIImage(named:" ")] with your names of images and adjust the TimeInterval(1.3). Enjoy it.

Resources