How to animate UIBezierPath inside of draw(_:)? - ios

I have a task to draw rectangle in draw(_:) method and to animate height of it.
var height = 0
override func draw(_ rect: CGRect) {
let rect = CGRect(x: 0, y: 0, width: 100, height: height)
let rectanglePath = UIBezierPath(rect: rect)
UIColor.green.setFill()
rectanglePath.fill()
}
And I'm looking to animate something like this:
func animate() {
UIView.animate(withDuration: 2) {
self.height = 300
}
}
I also can use CAShapeLayer and CABasicAnimation, but I don't know how to draw it in draw(_:) method. Using draw(_:) method is required in this task :(

class View: UIView{
override func draw(_ rect: CGRect)
{
let rectanglePath = UIBezierPath(rect: self.frame)
UIColor.green.setFill()
rectanglePath.fill()
}}
class ViewController: UIViewController{
override func viewDidLoad()
{
super.viewDidLoad()
let subview = View(frame: CGRect(x: 0, y: 0, width: 100, height: 0))
self.view.addSubview(subview)
UIViewPropertyAnimator.runningPropertyAnimator(
withDuration: 2,
delay: 0,
options: .curveLinear,
animations: {self.view.subviews[0].frame = CGRect(x: 0, y: 0, width: 100, height: 100)}
)
}}

Related

How to create a gradient which goes from full color to opacity of 0 in iOS?

I have looked here, but based on their example, which I tried and show below, did not work. It was not able to accomplish the following. I am looking to create a gradient from full black to a full black with opacity of 0:
#IBDesignable
final class GradientView: UIView {
#IBInspectable var startColor: UIColor = UIColor.clear
#IBInspectable var endColor: UIColor = UIColor.clear
override func draw(_ rect: CGRect) {
let gradient: CAGradientLayer = CAGradientLayer()
// gradient.frame = bounds
gradient.frame = CGRect(x: CGFloat(0),
y: CGFloat(0),
width: superview!.frame.size.width,
height: superview!.frame.size.height)
gradient.colors = [startColor.cgColor, endColor.cgColor]
gradient.zPosition = -1
layer.addSublayer(gradient)
}
}
How can I achieve this, preferably in the interface builder?
Following is my implementation
#IBDesignable
final class GradientView: UIView {
override func draw(_ rect: CGRect) {
//// General Declarations
let context = UIGraphicsGetCurrentContext()!
//// Gradient Declarations
let gradient = CGGradient(colorsSpace: nil, colors: [UIColor.white.cgColor,
UIColor.white.blended(withFraction: 0.5, of: UIColor.black).cgColor,
UIColor.black.cgColor] as CFArray, locations: [0, 0.51, 0.89])!
//// Rectangle Drawing
let rectangleRect = CGRect(x: frame.minX + 11, y: frame.minY + 8, width: 85, height: 54)
let rectanglePath = UIBezierPath(rect: rectangleRect)
context.saveGState()
rectanglePath.addClip()
context.drawLinearGradient(gradient,
start: CGPoint(x: rectangleRect.midX, y: rectangleRect.minY),
end: CGPoint(x: rectangleRect.midX, y: rectangleRect.maxY),
options: [])
context.restoreGState()
}
}
What I ended up doing is the following:
private func setupGradient() {
//Below for container
gradientViewContainer.frame = CGRect(x: 0, y: 152, width: 117, height: 38)
gradientViewContainer.isHidden = true
//Below for actual gradient
gradientLayer.frame = CGRect(x: 0, y: 152, width: gradientViewContainer.frame.width, height: gradientViewContainer.frame.height)
gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.colors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.7).cgColor, UIColor.black.withAlphaComponent(0.7).cgColor, UIColor.black.withAlphaComponent(1.0).cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientViewContainer.layer.insertSublayer(gradientLayer, at: 0)
gradientViewContainer.alpha = 0.65
}
Then in viewDidLoad:
setupGradient()
myView.layer.addSublayer(gradientLayer)

UIView cut out any number of transparent holes

I want to a UIView can have any number of transparent holes. I tried UIBezierPath with UsesEvenOddFillRule to be true. But there is no luck, with one hole is OK, with any number of holes, it doesn't work.
You can draw a circles/ellipse with below code:
class HolyView: UIView {
var holes: [CGRect] = [] {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
super.draw(rect)
for hole in holes {
let intersection = rect.intersection(hole)
let context = UIGraphicsGetCurrentContext()
if intersection.intersects(rect) {
context?.setFillColor(self.backgroundColor?.cgColor ?? UIColor.clear.cgColor)
context?.setBlendMode(CGBlendMode.clear)
context?.fillEllipse(in: intersection)
}
}
}
}
OR Using BezierPath:
class BeziHolyView: UIView {
var holes: [CGRect] = [] {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let context = UIGraphicsGetCurrentContext()
context?.clear(rect)
let clipPath = UIBezierPath(rect: self.bounds)
for hole in holes {
clipPath.append(UIBezierPath(roundedRect: hole, cornerRadius: hole.height/2))
}
clipPath.usesEvenOddFillRule = true
clipPath.addClip()
backgroundColor?.setFill()
clipPath.fill()
}
}
Usage:
let view = HolyView/BeziHolyView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = UIColor.green
view.holes = [CGRect(x: 40, y: 20, width: 20, height: 20),
CGRect(x: 80, y: 30, width: 20, height: 20)]
Result:

Touch event on Visible Portion of Views ios swift 4

I am working of Photo Collage which is in Swift4 , I had created collage using UIBezierPath as Below
I have 5 scroll views in Storyboard and the sequence of Scrollviews as Below
Using Following code I am creating Shapes :
var path1 = UIBezierPath()
path1.move(to: CGPoint(x: 0, y: 0))
path1.addLine(to: CGPoint(x: superView.frame.width / 2, y: 0))
path1.addLine(to: CGPoint(x: 0, y: superView.frame.width / 2))
path1.addLine(to: CGPoint(x: 0, y: 0))
var borderPathRef1 = path1.cgPath
var borderShapeLayer1 = CAShapeLayer()
borderShapeLayer1.path = borderPathRef1
scroll1.layer.mask = borderShapeLayer1
scroll1.layer.masksToBounds = true
var path2 = UIBezierPath()
path2.move(to: CGPoint(x: 0, y: 0))
path2.addLine(to: CGPoint(x: superView.frame.width / 2, y: 0))
path2.addLine(to: CGPoint(x: superView.frame.width / 2, y: superView.frame.width / 2))
path2.addLine(to: CGPoint(x: 0, y: 0))
var borderPathRef2 = path2.cgPath
var borderShapeLayer2 = CAShapeLayer()
borderShapeLayer2.path = borderPathRef2
scroll2.layer.mask = borderShapeLayer2
scroll2.layer.masksToBounds = true
Now the issue is I am not able to get touch event of Scrollviews as Scroll5 is on top. I want to get Touch on Overlapped views like Scroll1, Scroll2 and so on. In Short I need touch event for particular view on the portion of area where the view is visible.
See The Image Below Where I want Touch for Views.
How can I get touch on Overlapped Views?
Please Help!
You have to be sure that you set the superView of scrollview to isUserInterfaceEnabled=true first.
To get overlapped views touch event:
here is my code:
class CustomView: UIView {
let view1: UIView
let view2: UIView
let view3: UIView
let view4: UIView
let view5: UIView
override init(frame: CGRect) {
view1 = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view1.backgroundColor = UIColor.black
view1.isUserInteractionEnabled = true
view2 = UIView(frame: CGRect(x: 100, y: 0, width: 100, height: 100))
view2.backgroundColor = UIColor.orange
view2.isUserInteractionEnabled = true
view3 = UIView(frame: CGRect(x: 0, y: 100, width: 100, height: 100))
view3.backgroundColor = UIColor.blue
view3.isUserInteractionEnabled = true
view4 = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
view4.backgroundColor = UIColor.brown
view4.isUserInteractionEnabled = true
view5 = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view5.tag = 121
view5.backgroundColor = UIColor.red
super.init(frame: frame)
self.addSubview(view1)
self.addSubview(view2)
self.addSubview(view3)
self.addSubview(view4)
self.addSubview(view5)
view1.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(touchAction)))
}
#objc func touchAction () {
print("----------- touch view 1")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if (view1.point(inside: self.convert(point, to: view1), with: event)) {
return view1
}
return self
}
}
The function hitTest decides that which view you are touching, so you just need to return the overlapped view.

Unable to properly animate the frame of a subview within a parent view

I am trying to animate the frame change of a subview within a view. Within the animation block I am setting the size of the subview to half its original width and height. However, when I run the code, the subview unexpectedly started with a larger size and shrunk to its original size instead of becoming half its original size. I am using the code below:
My custom view:
import UIKit
class TestView: UIView {
var testSubview: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.yellow
testSubview = UIView(frame: .zero)
testSubview.backgroundColor = UIColor.red
addSubview(testSubview)
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
override func layoutSubviews() {
super.layoutSubviews()
testSubview.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
}
func testAnimate() {
UIView.animate(withDuration: 3, delay: 0, options: [], animations: { [unowned self] in
self.testSubview.bounds.size = CGSize(width: 50, height: 50)
}, completion: nil)
}
}
My view controller:
import UIKit
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let testView = TestView()
view.addSubview(testView)
testView.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
testView.center = view.center
testView.layoutIfNeeded()
testView.testAnimate()
}
}
Am I doing something wrongly?
The problem is that your view gets laid out two times, you can check that by printing a debug message inside layoutSubviews().
I would suggest something like this, setting a size variable and use that in both layoutSubviews() and testAnimate():
class TestView: UIView {
var testSubview: UIView!
var mySize = CGSize(width: 100, height: 100)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.yellow
testSubview = UIView(frame: .zero)
testSubview.backgroundColor = UIColor.red
addSubview(testSubview)
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
override func layoutSubviews() {
super.layoutSubviews()
testSubview.frame = CGRect(x: 0, y: 0, width: mySize.width, height: mySize.height)
}
func testAnimate() {
UIView.animate(withDuration: 3, delay: 0, options: [], animations: { [unowned self] in
self.mySize = CGSize(width: 50, height: 50)
self.testSubview.bounds.size = self.mySize
}, completion: nil)
}
}
EDIT: Reformulating my whole answer because there are many things I feel should be changed here.
first of all the difference between setting the bounds of a view and it's frame.
let view = UIView()
view.frame = CGRect(x: 0, y: 0, width: 200, height: 200) // This will set the position and size this view will take relative to it's future parents view
view.bounds = CGRect(x: 50, y: 50, width: 100, height: 100) // This will set position and size of it's own content (i.e. it's background) relative to it's own view
Then comes your testView class variables. Unless you are certain that a variable won't be nil don't use UIView! as the typing, you would end up missing some initialisers and thus your class would not work consistently.
Consider init methods as default values for your variables and you can simply alter them afterwards.
import UIKit
class TestView: UIView {
var testSubview: UIView
override init(frame: CGRect) {
// Initialise your variables before calling super
testSubview = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
testSubview.backgroundColor = UIColor.red
super.init(frame: frame)
// Once super is called you can start using class methods or variable such as addSubview or backgroundColor
backgroundColor = UIColor.yellow
addSubview(testSubview)
}
required init?(coder aDecoder: NSCoder) {
testSubview = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
testSubview.backgroundColor = UIColor.blue
super.init(coder: aDecoder)
backgroundColor = UIColor.yellow
addSubview(testSubview)
}
}
now finally to the animation part
import UIKit
class TestView: UIView {
var testSubview: UIView
func testAnimation() {
UIView.animate(withDuration: 3, animations: {
self.testSubview.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) // Simply make the view half its size over 3 seconds
}) { (_) in
UIView.animate(withDuration: 3, animations: {
self.testSubview.transform = CGAffineTransform(scaleX: 1, y: 1) // Upon completion, resize the view back to it's original size
})
}
}
}
or if you really want to use CGRect to do animation on it's position
import UIKit
class TestView: UIView {
var testSubview: UIView
func testAnimation() {
UIView.animate(withDuration: 3, animations: {
self.testSubview.frame = CGRect(x: 0, y: 0, width: 50, height: 50) // use frame not bounds to resize and reposition the frame
}) { (_) in
UIView.animate(withDuration: 3, animations: {
self.testSubview.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
})
}
}
}
you can test the effect of changing the bounds attribute as such
import UIKit
class ViewController: UIViewController {
var testView: TestView!
override func viewDidLoad() {
testView = TestView(frame: CGRect(origin: .zero, size: CGSize(width: 200, height: 200)))
testView.bounds = CGRect(x: 50, y: 50, width: 100, height: 100)
testView.clipsToBounds = true
view.addSubview(testView)
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
testView.testAnimation()
}
}
simply play around with clipsToBounds, testView.bounds, testView.subView.frame and testView = TestView(frame: CGRect(x:y:width:height) to see all of this

Custom progress view

I want to do a custom progress view for my iOS app, with 2 dots. Here is my code:
import UIKit
#IBDesignable
class StepProgressView: UIView {
#IBInspectable var progress: Float = 0
var progressColor = UIColor.blackColor()
var bgColor = UIColor.whiteColor()
override func layoutSubviews() {
self.backgroundColor = UIColor.clearColor()
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
let height = frame.height-8
let circle1 = UIView(frame: CGRect(x: frame.width*(1/3), y: 0, width: frame.height, height: frame.height))
circle1.backgroundColor = bgColor
circle1.layer.cornerRadius = frame.height/2
addSubview(circle1)
let circle2 = UIView(frame: CGRect(x: frame.width*(2/3), y: 0, width: frame.height, height: frame.height))
circle2.backgroundColor = bgColor
circle2.layer.cornerRadius = frame.height/2
addSubview(circle2)
let bgView = UIView(frame: CGRect(x: height/2, y: 4, width: frame.width-height/2, height: height))
bgView.backgroundColor = bgColor
bgView.layer.cornerRadius = height/2
addSubview(bgView)
let progressView = UIView(frame: CGRect(x: 0, y: 4, width: frame.width*CGFloat(progress), height: height))
progressView.backgroundColor = progressColor
progressView.layer.cornerRadius = height/2
addSubview(progressView)
}
}
The result:
However, as you can see, the circles aren't "filled" when the progression pass over one of them, and I don't know how to do that. I could create another view but I don't know how to handle the corner radius.
Can you help me ?
Thanks

Resources