Overriding draw function in UIView class of iOS - ios

I created a class from UI view and below is code.
class HudView: UIView {
var text = ""
class func hud(inView view: UIView, animated: Bool) -> HudView {
let hudView = HudView(frame: view.bounds)
hudView.isOpaque = false
view.addSubview(hudView)
hudView.show(animated: animated)
return hudView
}
override func draw(_ rect: CGRect) {
let boxWidth: CGFloat = 96
let boxHeight: CGFloat = 96
let boxRect = CGRect(x: round((bounds.size.width - boxWidth) / 2), y: round((bounds.size.height - boxHeight) / 2), width: boxWidth, height: boxHeight)
let roundRect = UIBezierPath(roundedRect: boxRect, cornerRadius: 10)
UIColor(white: 0.3, alpha: 0.8).setFill()
roundRect.fill()
if let image = UIImage(named: "Checkmark") {
let imagePoint = CGPoint(x: center.x - round(image.size.width / 2), y: center.y - round(image.size.height / 2))
image.draw(at: imagePoint)
}
let attribs = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor.white]
let textSize = text.size(withAttributes: attribs)
let textPoint = CGPoint(x: center.x - round(textSize.width / 2), y: center.y - round(textSize.height / 2) + boxHeight / 4)
text.draw(at: textPoint, withAttributes: attribs)
}
func show(animated: Bool) {
if animated {
alpha = 0
transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
UIView.animate(withDuration: 0.3, animations: {
self.alpha = 1
self.transform = CGAffineTransform.identity
})
}
}
When I am creating object of this class, I observed that I don't need to call "draw" function separately and it still draws from this function. Could you help what is the reason, I don't need to call this function on HudView's object.
Below is code for creating object from class and even without calling "draw" func, it is still able to draw with given specifications.
let hudView = HudView.hud(inView: navigationController!.view, animated: true)
hudView.text = "Tagged"

from here: https://developer.apple.com/documentation/uikit/uiview/1622529-draw
"This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself"

Related

How to make custom ripples like Square, Stare and other custom shapes in Swift 5?

I have facing issue to make ripples in Square and Stare figure like YRipple
Please help me and suggestion always welcome.
One easy way to achieve this is to use UIView animations. Each ripple is simply an instance of UIView. The shape can then be simply defined, drawn in one of many ways. I am using the override of draw rect method:
class RippleEffectView: UIView {
func addRipple(at location: CGPoint) {
let minRadius: CGFloat = 5.0
let maxRadius: CGFloat = 100.0
let startFrame = CGRect(x: location.x - minRadius, y: location.y - minRadius, width: minRadius*2.0, height: minRadius*2.0)
let endFrame = CGRect(x: location.x - maxRadius, y: location.y - maxRadius, width: maxRadius*2.0, height: maxRadius*2.0)
let view = ShapeView(frame: startFrame)
view.shape = .star(cornerCount: 5)
view.backgroundColor = .clear
view.contentMode = .redraw
view.strokeColor = .black
view.strokeWidth = 5.0
addSubview(view)
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.allowUserInteraction]) {
view.frame = endFrame
view.alpha = 0.0
} completion: { _ in
view.removeFromSuperview()
}
}
}
private class ShapeView: UIView {
var fillColor: UIColor?
var strokeColor: UIColor?
var strokeWidth: CGFloat = 0.0
var shape: Shape = .rectangle
override func draw(_ rect: CGRect) {
super.draw(rect)
let path = generatePath()
path.lineWidth = strokeWidth
if let fillColor = fillColor {
fillColor.setFill()
path.fill()
}
if let strokeColor = strokeColor {
strokeColor.setStroke()
path.stroke()
}
}
private func generatePath() -> UIBezierPath {
switch shape {
case .rectangle: return UIBezierPath(rect: bounds.insetBy(dx: strokeWidth*0.5, dy: strokeWidth*0.5))
case .oval: return UIBezierPath(ovalIn: bounds.insetBy(dx: strokeWidth*0.5, dy: strokeWidth*0.5))
case .anglesOnCircle(let cornerCount):
guard cornerCount > 2 else { return .init() }
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height)*0.5 - strokeWidth*0.5
let path = UIBezierPath()
for index in 0..<cornerCount {
let angle = CGFloat(index)/CGFloat(cornerCount) * (.pi*2.0)
let point = CGPoint(x: center.x + cos(angle)*radius,
y: center.y + sin(angle)*radius)
if index == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.close()
return path
case .star(let cornerCount):
guard cornerCount > 2 else { return .init() }
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let outerRadius = min(bounds.width, bounds.height)*0.5 - strokeWidth*0.5
let innerRadius = outerRadius*0.7
let path = UIBezierPath()
for index in 0..<cornerCount*2 {
let angle = CGFloat(index)/CGFloat(cornerCount) * .pi
let radius = index.isMultiple(of: 2) ? outerRadius : innerRadius
let point = CGPoint(x: center.x + cos(angle)*radius,
y: center.y + sin(angle)*radius)
if index == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.close()
return path
}
}
}
private extension ShapeView {
enum Shape {
case rectangle
case oval
case anglesOnCircle(cornerCount: Int)
case star(cornerCount: Int)
}
}
I used it in a view controller where I replaced main view with this ripple view in Storyboard.
class ViewController: UIViewController {
private var rippleView: RippleEffectView? { view as? RippleEffectView }
override func viewDidLoad() {
super.viewDidLoad()
rippleView?.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
}
#objc private func onTap(_ recognizer: UIGestureRecognizer) {
let location = recognizer.location(in: rippleView)
rippleView?.addRipple(at: location)
}
}
I hope the code speaks for itself. It should be no problem to change colors. You could apply some rotation by using transform on each ripple view...
You could even use images instead of shapes. If image is set to be as templates you could even change colors using tint property on image view... So limitless possibilities.

Add subview on a parent drawn through UIBezierPath

I've a custom UITabBar. Its bar has a simple but customised shape: its height is bigger then default one, has rounded corners and (important) a shadow layer on the top.
The result is this:
Now I've to add an element that shows the selected section on the top of the bar, to achieve this:
The problem is that no matter the way I choose to add this element (add a subview to the bar or add a new sublayer) but the new element will always be drawn outside the corners. I suppose this is because I can't enable the clipping mask (if I enable the clipping mask I'll kill the shadow and also, more important, the bezierpath)
Do you have any tips for this?
Basically, the goal should be:
have an element that moves horizontally (animated) but cannot be drawn outside the parent (the tabbar)
Actually, the code to draw the custom tabBar is:
class CustomTabBar: UITabBar {
/// The layer that defines the custom shape
private var shapeLayer: CALayer?
/// The radius for the border of the bar
var borderRadius: CGFloat = 0
override func layoutSubviews() {
super.layoutSubviews()
// aspect and shadow
isTranslucent = false
backgroundColor = UIColor.white
tintColor = AZTheme.PaletteColor.primaryColor
shadowImage = nil
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.1
layer.shadowOffset = CGSize(width: 0, height: -1)
layer.shadowRadius = 10
}
override func draw(_ rect: CGRect) {
drawShape()
}
/// Draw and apply the custom shape to the bar
func drawShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.fillColor = AZTheme.tabBarControllerBackgroundColor.cgColor
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
}
// MARK: - Private functions
extension CustomTabBar {
/// Return the custom shape for the bar
internal func createPath() -> CGPath {
let height: CGFloat = self.frame.height
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addArc(withCenter: CGPoint(x: borderRadius, y: 0), radius: borderRadius, startAngle: CGFloat.pi, endAngle: CGFloat.pi * (3/2), clockwise: true)
path.addLine(to: CGPoint(x: frame.width - borderRadius, y: -borderRadius))
path.addArc(withCenter: CGPoint(x: frame.width - borderRadius, y: 0), radius: borderRadius, startAngle: CGFloat.pi * (3/2), endAngle: 0, clockwise: true)
path.addLine(to: CGPoint(x: frame.width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.close()
return path.cgPath
}
}
I solved splitting the owner of custom shape from the owner of the shadow in 2 different views. So I'm using 3 views achieve the goal.
CustomTabBar: has default size and casts shadow with offset.
|
└ SelectorContainer: is a view with custom shape (BezierPath) that is
positioned on the top of the TabBar to graphically "extend" the view
and have the feeling of a bigger TabBar. It has rounded corners on
the top-right, top-left margin. MaskToBounds enabled.
|
└ Selector: simple view that change the its origin through animation.
See the result here
The code:
class CustomTabBar: UITabBar {
/// The corner radius value for the top-left, top-right corners of the TabBar
var borderRadius: CGFloat = 0
/// Who is containing the selector. Is a subview of the TabBar.
private var selectorParent: UIView?
/// Who moves itself following the current section. Is a subview of ```selectorParent```.
private var selector: UIView?
/// The height of the ```selector```
private var selectorHeight: CGFloat = 5
/// The number of sections handled by the TabBarController.
private var numberOfSections: Int = 0
override func layoutSubviews() {
super.layoutSubviews()
isTranslucent = false
backgroundColor = UIColor.white
tintColor = AZTheme.PaletteColor.primaryColor
shadowImage = nil
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.1
layer.shadowOffset = CGSize(width: 0, height: -1)
layer.shadowRadius = 10
}
}
// MARK: - Private functions
extension CustomTabBar {
/// Create the selector element on the top of the TabBar
func setupSelectorView(numberOfSections: Int) {
self.numberOfSections = numberOfSections
// delete previous subviews (if exist)
if let selectorContainer = self.selectorParent {
selectorContainer.removeFromSuperview()
self.selector?.removeFromSuperview()
self.selectorParent = nil
self.selector = nil
}
// SELECTOR CONTAINER
let selectorContainerRect: CGRect = CGRect(x: 0,
y: -borderRadius,
width: frame.width,
height: borderRadius)
let selectorContainer = UIView(frame: selectorContainerRect)
selectorContainer.backgroundColor = UIColor.white
selectorContainer.AZ_roundCorners([.topLeft, .topRight], radius: borderRadius)
selectorContainer.layer.masksToBounds = true
self.addSubview(selectorContainer)
// SELECTOR
let selectorRect: CGRect = CGRect(x: 0,
y: 0,
width: selectorContainer.frame.width / CGFloat(numberOfSections),
height: selectorHeight)
let selector = UIView(frame: selectorRect)
selector.backgroundColor = AZTheme.PaletteColor.primaryColor
selectorContainer.addSubview(selector)
// add views to hierarchy
self.selectorParent = selectorContainer
self.selector = selector
}
/// Animate the position of the selector passing the index of the new section
func animateSelectorTo(sectionIndex: Int) {
guard let selectorContainer = self.selectorParent, let selector = self.selector else { return }
selector.layer.removeAllAnimations()
let sectionWidth: CGFloat = selectorContainer.frame.width / CGFloat(numberOfSections)
let newCoord = CGPoint(x: sectionWidth * CGFloat(sectionIndex), y: selector.frame.origin.y)
UIView.animate(withDuration: 0.25, delay: 0, options: UIView.AnimationOptions.curveEaseOut, animations: { [weak self] in
self?.selector?.frame.origin = newCoord
}, completion: nil)
}
}

How to draw multiple horizontally circles in rectangle (UIButton or UIControl) Swift iOS

How to draw about three circle in horizontally area with main and ring color in rectangle. I need to create custom button with this circles, something like this:
Is there any good way to do this?
We can design such kind of views with UIStackView in very ease manner.
Take a stackView, set its alignment to center, axis to horizontal and distribution to fill. Create a UILabel/UIButton/UIImageView or even UIView and add rounded radius and border to it. Finally, add those views to the main stackView.
Try this.
override func viewDidLoad() {
super.viewDidLoad()
//Setup stackView
let myStackView = UIStackView()
myStackView.axis = .horizontal
myStackView.alignment = .center
myStackView.distribution = .fillEqually
myStackView.spacing = 8
view.addSubview(myStackView)
//Setup circles
let circle_1 = circleLabel()
let circle_2 = circleLabel()
let circle_3 = circleLabel()
myStackView.addArrangedSubview(circle_1)
myStackView.addArrangedSubview(circle_2)
myStackView.addArrangedSubview(circle_3)
myStackView.translatesAutoresizingMaskIntoConstraints = false
myStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0).isActive = true
myStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0).isActive = true
}
func circleLabel() -> UILabel {
let label = UILabel()
label.backgroundColor = UIColor.red
label.layer.cornerRadius = 12.5
label.layer.masksToBounds = true
label.layer.borderColor = UIColor.orange.cgColor
label.layer.borderWidth = 3.0
label.widthAnchor.constraint(equalToConstant: 25.0).isActive = true
label.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
return label
}
To make a Single Circle like that, you need to make use of UIBezierPath and CAShapeLayer .
let outerCirclePath = UIBezierPath(arcCenter: CGPoint(x: 100,y: 100), radius: CGFloat(50), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
let outerCircleShapeLayer = CAShapeLayer()
outerCircleShapeLayer.path = outerCirclePath.cgPath
outerCircleShapeLayer.fillColor = UIColor.white.cgColor
outerCircleShapeLayer.lineWidth = 3.0
view.layer.addSublayer(outerCircleShapeLayer)
// Drawing the inner circle
let innerCirclePath = UIBezierPath(arcCenter: CGPoint(x: 100,y: 100), radius: CGFloat(40), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
let innerCircleShapeLayer = CAShapeLayer()
innerCircleShapeLayer.path = innerCirclePath.cgPath
innerCircleShapeLayer.fillColor = UIColor.blue.cgColor
view.layer.addSublayer(innerCircleShapeLayer)
I have attached an image below for the Playground version of it .
Just play around with arcCenter and radius values and you will get the desired output
My team helped me and here is solution to create this with dynamically changing state of circles (with different stroke and fill colors):
import UIKit
#IBDesignable
class CirclesButton: UIControl {
#IBInspectable
var firstCircle: Bool = false {
didSet {
setNeedsDisplay()
}
}
#IBInspectable
var secondCircle: Bool = false {
didSet {
setNeedsDisplay()
}
}
#IBInspectable
var thirdCircle: Bool = false {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
// get context
guard let context = UIGraphicsGetCurrentContext() else { return }
// make configurations
context.setLineWidth(1.0);
context.setStrokeColor(UIColor.white.cgColor)
context.setFillColor(red: 0.0, green: 0.58, blue: 1.0, alpha: 1.0)
// find view center
let dotSize:CGFloat = 11.0
let viewCenter = CGPoint(x: rect.midX, y: rect.midY)
// find personal dot rect
var dotRect = CGRect(x: viewCenter.x - dotSize / 2.0, y: viewCenter.y - dotSize / 2.0, width: dotSize, height: dotSize)
if secondCircle {
context.fillEllipse(in: dotRect)
}
context.strokeEllipse(in: dotRect)
// find global notes rect
dotRect = CGRect(x: viewCenter.x - dotSize * 1.5 - 4.0, y: viewCenter.y - dotSize / 2.0, width: dotSize, height: dotSize)
if firstCircle {
context.fillEllipse(in: dotRect)
}
context.strokeEllipse(in: dotRect)
// find music rect
dotRect = CGRect(x: viewCenter.x + dotSize / 2.0 + 4.0, y: viewCenter.y - dotSize / 2.0, width: dotSize, height: dotSize)
if thirdCircle {
context.setFillColor(red: 0.0, green: 1.0, blue: 0.04, alpha: 1.0)
context.fillEllipse(in: dotRect)
}
context.strokeEllipse(in: dotRect)
}
}
It will looks like: CirclesButton
Сode:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let buttonSize: CGFloat = 80
let firstButton = CustomButton(position: CGPoint(x: 0, y: 0), size: buttonSize, color: .blue)
self.view.addSubview(firstButton)
let secondButton = CustomButton(position: CGPoint(x: firstButton.frame.maxX, y: 0), size: buttonSize, color: .blue)
self.view.addSubview(secondButton)
let thirdButton = CustomButton(position: CGPoint(x: secondButton.frame.maxX, y: 0), size: buttonSize, color: .green)
self.view.addSubview(thirdButton)
}
}
class CustomButton: UIButton {
init(position: CGPoint, size: CGFloat, color: UIColor) {
super.init(frame: CGRect(x: position.x, y: position.y, width: size, height: size))
self.backgroundColor = color
self.layer.cornerRadius = size / 2
self.clipsToBounds = true
self.layer.borderWidth = 4.0 // make it what ever you want
self.layer.borderColor = UIColor.white.cgColor
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
}
You can handle button tapped like:
override func viewDidLoad() {
super.viewDidLoad()
firstButton.addTarget(self, action: #selector(handleFirstButton), for: .touchUpInside)
}
#objc func handleFirstButton(sender: UIButton) {
print("first button tapped")
}
Best and Universal Solution for **Button or Label creation (Fully Dynamic)**
var x = 10
var y = 5
var buttonHeight = 40
var buttonWidth = 40
for i in 0..<3 {
let roundButton = UIButton(frame: CGRect(x: x, y: y, width: buttonWidth, height: buttonHeight))
roundButton.setTitle("Butt\(i)", for: .normal)
roundButton.layer.cornerRadius = roundButton.bounds.size.height/2
yourButtonBackView.addSubview(roundButton)
x = x + buttonWidth + 10
if x >= Int(yourButtonBackView.frame.width - 30) {
y = y + buttonHeight + 10
x = 10
}
}

UIView horizontal bar animation in swift

I am working on this animation where a number will be received every second and progress bar has to fill or go down based on the double value.
I have created the views and have added all the views in the UIStackView. Also made the outlet collection for all the views. (sorting them by the tag and making them round rect).
I can loop the views and change their background color but trying to see if there is a better way to do it. Any suggestions?
Thanks
So how you are doing it is fine. Here would be two different ways. The first with Core Graphics. You may want to update methods and even make the color gradient in the sublayer.
1st Way
import UIKit
class Indicator: UIView {
var padding : CGFloat = 5.0
var minimumSpace : CGFloat = 4.0
var indicators : CGFloat = 40
var indicatorColor : UIColor = UIColor.lightGray
var filledIndicatorColor = UIColor.blue
var currentProgress = 0.0
var radiusFactor : CGFloat = 0.25
var fillReversed = false
override init(frame: CGRect) {
super.init(frame: frame)
setUp(animated: false)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp(animated: false)
backgroundColor = UIColor.green
}
func updateProgress(progress:Double, animated:Bool) {
currentProgress = progress
setUp(animated: animated)
}
private func setUp(animated:Bool){
// internal space
let neededPadding = (indicators - 1) * minimumSpace
//calculate height and width minus padding and space since vertical
let height = (bounds.height - neededPadding - (padding * 2.0)) / indicators
let width = bounds.width - padding * 2.0
if animated == true{
let trans = CATransition()
trans.type = kCATransitionFade
trans.duration = 0.5
self.layer.add(trans, forKey: nil)
}
layer.sublayers?.removeAll()
for i in 0...Int(indicators - 1.0){
let indicatorLayer = CALayer()
indicatorLayer.frame = CGRect(x: padding, y: CGFloat(i) * height + padding + (minimumSpace * CGFloat(i)), width: width, height: height)
//haha i don't understand my logic below but it works hahaha
// i know it has to go backwards
if fillReversed{
if CGFloat(1 - currentProgress) * self.bounds.height < indicatorLayer.frame.origin.y{
indicatorLayer.backgroundColor = filledIndicatorColor.cgColor
}else{
indicatorLayer.backgroundColor = indicatorColor.cgColor
}
}else{
if CGFloat(currentProgress) * self.bounds.height > indicatorLayer.frame.origin.y{
indicatorLayer.backgroundColor = indicatorColor.cgColor
}else{
indicatorLayer.backgroundColor = filledIndicatorColor.cgColor
}
}
indicatorLayer.cornerRadius = indicatorLayer.frame.height * radiusFactor
indicatorLayer.masksToBounds = true
self.layer.addSublayer(indicatorLayer)
}
}
//handle rotation
override func layoutSubviews() {
super.layoutSubviews()
setUp(animated: false)
}
}
The second way is using CAShapeLayer and the benefit is that the progress will get a natural animation.
import UIKit
class Indicator: UIView {
var padding : CGFloat = 5.0
var minimumSpace : CGFloat = 4.0
var indicators : CGFloat = 40
var indicatorColor : UIColor = UIColor.lightGray
var filledIndicatorColor = UIColor.blue
var currentProgress = 0.0
var radiusFactor : CGFloat = 0.25
private var progressLayer : CALayer?
private var shapeHoles : CAShapeLayer?
override init(frame: CGRect) {
super.init(frame: frame)
transparentDotsAndProgress()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
transparentDotsAndProgress()
}
func updateProgress(progress:Double) {
if progress <= 1 && progress >= 0{
currentProgress = progress
transparentDotsAndProgress()
}
}
//handle rotation
override func layoutSubviews() {
super.layoutSubviews()
transparentDotsAndProgress()
}
func transparentDotsAndProgress(){
self.layer.masksToBounds = true
let neededPadding = (indicators - 1) * minimumSpace
//calculate height and width minus padding and space since vertical
let height = (bounds.height - neededPadding - (padding * 2.0)) / indicators
let width = bounds.width - padding * 2.0
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height), cornerRadius: 0)
path.usesEvenOddFillRule = true
for i in 0...Int(indicators - 1.0){
let circlePath = UIBezierPath(roundedRect: CGRect(x: padding, y: CGFloat(i) * height + padding + (minimumSpace * CGFloat(i)), width: width, height: height), cornerRadius: height * radiusFactor)
path.append(circlePath)
}
if progressLayer == nil{
progressLayer = CALayer()
progressLayer?.backgroundColor = filledIndicatorColor.cgColor
self.layer.addSublayer(progressLayer!)
}
progressLayer?.frame = CGRect(x: 0, y: -self.bounds.height - padding + CGFloat(currentProgress) * self.bounds.height, width: bounds.width, height: bounds.height)
self.shapeHoles?.removeFromSuperlayer()
shapeHoles = CAShapeLayer()
shapeHoles?.path = path.cgPath
shapeHoles?.fillRule = kCAFillRuleEvenOdd
shapeHoles?.fillColor = UIColor.white.cgColor
shapeHoles?.strokeColor = UIColor.clear.cgColor
self.layer.backgroundColor = indicatorColor.cgColor
self.layer.addSublayer(shapeHoles!)
}
}
Both of these ways should work but the advantage of the CAShapeLayer is you get a natural animation.
I'm a firm believer in learning through solving organic problems and slowly building my global knowledge on a subject. So I'm afraid I don't have any good tutorials for you.
Here is an example that will jump start you, though.
// For participating in Simulator's "slow animations"
#_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float
import UIKit
#IBDesignable
class VerticalProgessView: UIControl {
#IBInspectable
var numberOfSegments: UInt = 0
#IBInspectable
var verticalSegmentGap: CGFloat = 4.0
#IBInspectable
var outerColor: UIColor = UIColor(red: 33, green: 133, blue: 109)
#IBInspectable
var unfilledColor: UIColor = UIColor(red: 61, green: 202, blue: 169)
#IBInspectable
var filledColor: UIColor = UIColor.white
private var _progress: Float = 0.25
#IBInspectable
open var progress: Float {
get {
return _progress
}
set {
self.setProgress(newValue, animated: false)
}
}
let progressLayer = CALayer()
let maskLayer = CAShapeLayer()
var skipLayoutSubviews = false
open func setProgress(_ progress: Float, animated: Bool) {
if progress < 0 {
_progress = 0
} else if progress > 1.0 {
_progress = 1
} else {
// Clamp the percentage to discreet values
let discreetPercentageDistance: Float = 1.0 / 28.0
let nearestProgress = discreetPercentageDistance * round(progress/discreetPercentageDistance)
_progress = nearestProgress
}
CATransaction.begin()
CATransaction.setCompletionBlock { [weak self] in
self?.skipLayoutSubviews = false
}
if !animated {
CATransaction.setDisableActions(true)
} else {
CATransaction.setAnimationDuration(0.25 * Double(UIAnimationDragCoefficient()))
}
let properties = progressLayerProperties()
progressLayer.bounds = properties.bounds
progressLayer.position = properties.position
skipLayoutSubviews = true
CATransaction.commit() // This triggers layoutSubviews
}
override func prepareForInterfaceBuilder() {
awakeFromNib()
}
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = UIColor.clear
self.layer.backgroundColor = unfilledColor.cgColor
// Initialize and add the progressLayer
let properties = progressLayerProperties()
progressLayer.bounds = properties.bounds
progressLayer.position = properties.position
progressLayer.backgroundColor = filledColor.cgColor
self.layer.addSublayer(progressLayer)
// Initialize and add the maskLayer (it has the holes)
maskLayer.frame = self.layer.bounds
maskLayer.fillColor = outerColor.cgColor
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.path = maskPath(for: maskLayer.bounds)
self.layer.addSublayer(maskLayer)
// Layer hierarchy
// self.maskLayer
// self.progressLayer
// self.layer
}
override func layoutSubviews() {
super.layoutSubviews()
if skipLayoutSubviews {
// Crude but effective, not fool proof though
skipLayoutSubviews = false
return
}
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// Doesn't work for 180° rotations
let duration = UIApplication.shared.statusBarOrientationAnimationDuration * Double(UIAnimationDragCoefficient())
CATransaction.begin()
CATransaction.setAnimationTimingFunction(timingFunction)
CATransaction.setAnimationDuration(duration)
let properties = progressLayerProperties()
progressLayer.bounds = properties.bounds
progressLayer.position = properties.position
let size = self.bounds.size
let anchorPoint = CGPoint(x: 0.5, y: 1.0)
maskLayer.anchorPoint = anchorPoint
maskLayer.bounds = CGRect(origin: CGPoint.zero, size: size)
maskLayer.position = CGPoint(x: size.width * anchorPoint.x, y: size.height * anchorPoint.y)
// Animate the segments
let pathChangeAnimation = CAKeyframeAnimation(keyPath: "path")
let finalPath = maskPath(for: maskLayer.bounds)
pathChangeAnimation.values = [maskLayer.path!, finalPath]
pathChangeAnimation.keyTimes = [0, 1]
pathChangeAnimation.timingFunction = timingFunction
pathChangeAnimation.duration = duration
pathChangeAnimation.isRemovedOnCompletion = true
maskLayer.add(pathChangeAnimation, forKey: "pathAnimation")
CATransaction.setCompletionBlock {
// CAAnimation's don't actually change the value
self.maskLayer.path = finalPath
}
CATransaction.commit()
}
// Provides a path that will mask out all the holes to show self.layer and the progressLayer behind
private func maskPath(for rect: CGRect) -> CGPath {
let horizontalSegmentGap: CGFloat = 5.0
let segmentWidth = rect.width - horizontalSegmentGap * 2
let segmentHeight = rect.height/CGFloat(numberOfSegments) - verticalSegmentGap + verticalSegmentGap/CGFloat(numberOfSegments)
let segmentSize = CGSize(width: segmentWidth, height: segmentHeight)
let segmentRect = CGRect(x: horizontalSegmentGap, y: 0, width: segmentSize.width, height: segmentSize.height)
let path = CGMutablePath()
for i in 0..<numberOfSegments {
// Literally, just move it down by the y value here
// this simplifies the math of calculating the starting points and what not
let transform = CGAffineTransform.identity.translatedBy(x: 0, y: (segmentSize.height + verticalSegmentGap) * CGFloat(i))
let segmentPath = UIBezierPath(roundedRect: segmentRect, cornerRadius: segmentSize.height / 2)
segmentPath.apply(transform)
path.addPath(segmentPath.cgPath)
}
// Without the outerPath, we'll end up with a bunch of squircles instead of a bunch of holes
let outerPath = CGPath(rect: rect, transform: nil)
path.addPath(outerPath)
return path
}
/// Provides the current and correct bounds and position for the progressLayer
private func progressLayerProperties() -> (bounds: CGRect, position: CGPoint) {
let frame = self.bounds
let height = frame.height * CGFloat(progress)
let y = frame.height * CGFloat(1 - progress)
let width = frame.width
let anchorPoint = maskLayer.anchorPoint
let bounds = CGRect(x: 0, y: 0, width: width, height: height)
let position = CGPoint(x: 0 + width * anchorPoint.x, y: y + height * anchorPoint.x)
return (bounds: bounds, position: position)
}
// TODO: Implement functions to further mimic UIProgressView
}
extension UIColor {
convenience init(red: Int, green: Int, blue: Int) {
self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1)
}
}
Using in a storyboard
Enjoy the magic

gestureRecognizer not working on added subview

I'm adding a subview in a view that holds multiple gesturerecognizer however when this view is being introduced the gesture's won't fire when the user taps inside this newly added view. It's a UIBezierPath that I'm introducing however. I've read that this could be way it doesn't work. My question is, is it possible to make the added UIBezierPath transparent in a way that the device will think the view below the drawn box is being tapped. I hope it makes sense what I'm saying.
This is the code for my UIBezierPath
class drawRecognizerIndicator{
let gestureContainer: UIView!
var view_gesture: UIView!
init(container: UIView){
gestureContainer = container
}
func drawRect(image: UIImage){
let screenSize = gestureContainer.bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
let gestureX = (screenWidth * 0.75) * 0.5
let gestureY = (screenWidth * 0.75) * 0.5
let gestureWidth = (screenWidth * 0.75)
let gestureHeight = (screenWidth * 0.75)
if(view_gesture != nil){
if(view_gesture.isDescendantOfView(gestureContainer)){
view_gesture.removeFromSuperview()
}
}
view_gesture = UIView(frame: CGRect(
x: (screenWidth * 0.5) - gestureX,
y: (screenHeight * 0.5) - gestureY ,
width: gestureWidth,
height: gestureHeight))
view_gesture.backgroundColor = UIColor.blackColor()
view_gesture.alpha = 0
let maskPath = UIBezierPath(roundedRect: view_gesture.bounds, byRoundingCorners: .AllCorners, cornerRadii: CGSize(width: 10, height: 10))
let maskLayer = CAShapeLayer(layer: maskPath)
maskLayer.frame = view_gesture.bounds
maskLayer.path = maskPath.CGPath
view_gesture.layer.mask = maskLayer
let view_gestureSize = view_gesture.bounds
let gestureIconWidth = view_gestureSize.width
let gestureIconHeight = view_gestureSize.height
let iconX = (gestureIconWidth * 0.8) * 0.5
let iconY = (gestureIconHeight * 0.8) * 0.5
let iconWidth = gestureIconWidth * 0.8
let iconHeight = gestureIconHeight * 0.8
let image_gesture = UIImageView(frame: CGRect(
x: (gestureIconWidth * 0.5) - iconX,
y: (gestureIconHeight * 0.5) - iconY,
width: iconWidth,
height: iconHeight))
image_gesture.contentMode = UIViewContentMode.ScaleAspectFit
image_gesture.image = image
image_gesture.userInteractionEnabled = true
view_gesture.addSubview(image_gesture)
view_gesture.userInteractionEnabled = true
gestureContainer.addSubview(view_gesture)
animateGestureNotification(view_gesture)
}
func animateGestureNotification(view_gesture: UIView){
UIView.animateWithDuration(0.5, delay: 0, options: UIViewAnimationOptions.CurveEaseIn, animations: {
view_gesture.alpha = 0.4
}, completion: nil)
UIView.animateWithDuration(0.5, delay: 2, options: UIViewAnimationOptions.CurveEaseOut, animations: {
view_gesture.alpha = 0
}, completion: nil)
}
}
Check the documentation for
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWithGestureRecognizer
otherGestureRecognizer: UIGestureRecognizer) -> Bool {...}
It helps to decide what happens when there are several gesture recognizers on the same view controller.
I would start by putting some print statements in there to see exactly which GRs are being called, and then decide which ones should return true.

Resources