CGPatternCallback crashes in iOS 12 - ios

The following class implements a view with a striped pattern background. It works fine in iOS 11, but crashes with EXC_BAD_ACCESS in iOS 12 on the call to load(as:) in the callback method.
Any pointers to what I'm doing wrong would be welcome.
import UIKit
class LinedView: UIView {
var colors: [UIColor] = [] {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
var patternCallback = CGPatternCallbacks(
version: 0,
drawPattern: {pointer, context in
guard let colors = pointer?.load(as: [UIColor].self) else { return }
for (index, color) in colors.enumerated() {
context.setFillColor(color.cgColor)
context.fill(CGRect(x: CGFloat(index) * 2 + CGFloat(index - 1) + 1, y: 0, width: 2, height: 1))
}
},
releaseInfo: nil
)
let width = CGFloat(colors.count * 3)
withUnsafeMutablePointer(to: &colors) {
guard let pattern = CGPattern(
info: $0,
bounds: .init(x: 0, y: 0, width: width, height: 1),
matrix: .identity,
xStep: width + 1,
yStep: 1,
tiling: .constantSpacingMinimalDistortion,
isColored: true,
callbacks: &patternCallback
) else { return }
if let patternSpace = CGColorSpace(patternBaseSpace: nil) {
context.setFillColorSpace(patternSpace)
var alpha: CGFloat = 1
context.setFillPattern(pattern, colorComponents: &alpha)
context.fill(rect)
}
}
}
}

Related

UIGraphicsImageRenderer used in a loop ends with memory issue [duplicate]

This question already has answers here:
Swift5 MacOS ImageResize memory issue
(2 answers)
Closed last year.
I have a simple function like this:
let filteredImages = //array of images
let numbersArray = //array of array of numbers for ex [[1, 2], [2, 3]], total number of elements is 57
for (index, numbers) in numbersArray.enumerated() {
print(">>>>1 INDEX: \(index)")
if let cardimage = Box.card(for: filteredImages, numbers: numbers) {
//nothing here for now
}
}
class Box {
class func card(for images: [UIImage], numbers: [Int]) -> CardImage? {
var filteredImages = [UIImage]()
for number in numbers {
guard images.count > number - 1 else {
return nil
}
filteredImages.append(images[number - 1])
}
if filteredImages.count < 8 {
return nil
}
let creator = ImageCreator(radius: 500, backgroundColor: UIColor.athensGray)
return creator.card(from: filteredImages)
}
}
class ImageCreator {
private let radius: CGFloat
private let backgroundColor: UIColor
private let sets: [Set] //Array of predefined objects of my type Set, some numbers like x, y, width, height, radius
init(radius: CGFloat, backgroundColor: UIColor) {
self.radius = radius
self.backgroundColor = backgroundColor
}
func card(from images: [UIImage]) -> CardImage {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: radius, height: radius))
var coordinates = [ImageCoordinate]()
let image = renderer.image { [weak self] context in
let size = renderer.format.bounds.size
self?.backgroundColor.setFill()
context.cgContext.fillEllipse(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
let set = sets.random
for (index, image) in images.enumerated() {
if let positions = set?.positions[index], let cgimage = image.flippedVertically?.cgImage {
context.cgContext.translateBy(x: CGFloat(positions.x), y: CGFloat(positions.y))
context.cgContext.rotate(by: CGFloat(positions.r))
context.cgContext.translateBy(x: CGFloat(-positions.w/2), y: CGFloat(-positions.h/2))
let width = CGFloat(positions.w)
let height = CGFloat(positions.h)
var newWidth: CGFloat = 0
var newHeight: CGFloat = 0
let imageWidth = image.size.width
let imageHeight = image.size.height
let ratio = imageWidth / imageHeight
if ratio > 1 {
newWidth = CGFloat(width)
newHeight = newWidth / imageWidth * imageHeight
} else {
newHeight = CGFloat(height)
newWidth = newHeight / imageHeight * imageWidth
}
context.cgContext.translateBy(x: CGFloat((width - newWidth) / 2), y: CGFloat((height - newHeight) / 2))
context.cgContext.draw(cgimage, in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
context.cgContext.translateBy(x: CGFloat(-(width - newWidth) / 2), y: CGFloat(-(height - newHeight) / 2))
context.cgContext.translateBy(x: CGFloat(positions.w/2), y: CGFloat(positions.h/2))
context.cgContext.rotate(by: CGFloat(-positions.r))
context.cgContext.translateBy(x: CGFloat(-positions.x), y: CGFloat(-positions.y))
coordinates.append(ImageCoordinate(x: positions.x, y: positions.y, w: Double(newWidth), h: Double(newHeight), r: positions.r))
}
}
}
return CardImage(image: image, coordinates: coordinates)
}
}
I think it is not complicated as much because I do here a lot of staff with delivered set of images (always 8 images), but when I do it with a loop for i in 1...57 it ends up with memory issue (app is closed after ~31 iterations). Why?
How can I avoid that? Is there a way to fix that problem?
I suspect the problem you are encountering is caused by accumulating too many temporary (autorelease) objects (in this case the CGImages in the card method) in the loop that is rendering your cards.
I would try adding an autoreleasepool around the rendering of the card images.
for (index, numbers) in numbersArray.enumerated() {
autoreleasepool {
print(">>>>1 INDEX: \(index)")
if let cardimage = Box.card(for: filteredImages, numbers: numbers) {
//nothing here for now
}
}
}

Animate UISlider's buffer track change?

Following an online article and using a github project I was able to create a UISlider with a second track (bufferTrack).
The only problem I am facing is with updating the burrerEndValue / value. It is not animated. How could I achieve a smooth animation on the UIBezierPath?
open class BufferSlider: UISlider {
open var bufferStartValue:Double = 0 {
didSet{
if bufferStartValue < 0.0 {
bufferStartValue = 0
}
if bufferStartValue > bufferEndValue {
bufferStartValue = bufferEndValue
}
self.setNeedsDisplay()
}
}
open var bufferEndValue:Double = 0 {
didSet{
if bufferEndValue > 1.0 {
bufferEndValue = 1
}
if bufferEndValue < bufferStartValue{
bufferEndValue = bufferStartValue
}
self.setNeedsDisplay()
}
}
open var baseColor:UIColor = UIColor.white
open var progressColor:UIColor?
open var bufferColor:UIColor?
open var customBorderWidth: Double = 0.1{
didSet{
if customBorderWidth < 0.1 {
customBorderWidth = 0.1
}
self.setNeedsDisplay()
}
}
open var sliderHeight: Double = 6 {
didSet{
if sliderHeight < 1 {
sliderHeight = 1
}
}
}
override open func setValue(_ value: Float, animated: Bool) {
super.setValue(value, animated: animated)
self.setNeedsDisplay()
}
override init(frame: CGRect) {
super.init(frame: frame)
updateView()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
updateView()
}
func updateView() {
baseColor = UIColor.white
progressColor = appColors.red
bufferColor = appColors.fadedRed
}
open override func trackRect(forBounds bounds: CGRect) -> CGRect {
var result = super.trackRect(forBounds: bounds)
result.size.height = 0.01
return result
}
open override func draw(_ rect: CGRect) {
baseColor.set()
let rect = self.bounds.insetBy(dx: CGFloat(customBorderWidth), dy: CGFloat(customBorderWidth))
let height = sliderHeight.CGFloatValue
let radius = height/2
let sliderRect = CGRect(x: rect.origin.x, y: rect.origin.y + (rect.height/2-radius), width: rect.width, height: rect.width) //default center
let path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: sliderRect.minX + radius, y: sliderRect.minY+radius), radius: radius, startAngle: CGFloat(Double.pi)/2, endAngle: -CGFloat(Double.pi)/2, clockwise: true)
path.addLine(to: CGPoint(x: sliderRect.maxX-radius, y: sliderRect.minY))
path.addArc(withCenter: CGPoint(x: sliderRect.maxX-radius, y: sliderRect.minY+radius), radius: radius, startAngle: -CGFloat(Double.pi)/2, endAngle: CGFloat(Double.pi)/2, clockwise: true)
path.addLine(to: CGPoint(x: sliderRect.minX + radius, y: sliderRect.minY+height))
baseColor.setStroke()
path.lineWidth = customBorderWidth.CGFloatValue
path.stroke()
path.fill()
path.addClip()
var fillHeight = sliderRect.size.height-customBorderWidth.CGFloatValue
if fillHeight < 0 {
fillHeight = 0
}
let fillRect = CGRect(
x: sliderRect.origin.x + sliderRect.size.width*CGFloat(bufferStartValue),
y: sliderRect.origin.y + customBorderWidth.CGFloatValue/2,
width: sliderRect.size.width*CGFloat(bufferEndValue-bufferStartValue),
height: fillHeight)
if let color = bufferColor { color.setFill() }
else if let color = self.superview?.tintColor{ color.setFill()}
else{ UIColor(red: 0.0, green: 122.0/255.0, blue: 1.0, alpha: 1.0).setFill() }
UIBezierPath(rect: fillRect).fill()
if let color = progressColor{
color.setFill()
let fillRect = CGRect(
x: sliderRect.origin.x,
y: sliderRect.origin.y + customBorderWidth.CGFloatValue/2,
width: sliderRect.size.width*CGFloat((value-minimumValue)/(maximumValue-minimumValue)),
height: fillHeight)
UIBezierPath(rect: fillRect).fill()
}
}
}
extension Double{
var CGFloatValue: CGFloat {
return CGFloat(self)
}
}
You can't animate the UIView instance when you implemented custom drawing for it in draw(rect:) function, because during animation self.layer.presentationLayer has to be drawn with interpolated values between oldValue and newValue, but your overridden logic of drawing draws always with the newly set value newValue.
You can do custom drawing with animations that you want only in CALayer instance.
Consider implementing of your drawing logic in BufferSliderLayer: CALayer.
For animations on the layer, you'd need to interpolate values that you want to animate, e.g. bufferEndValue and value.
In order to do that, you can refer to this article.
Then, just add BufferSliderLayer onto your BufferSlider view's layer in slider's init(frame:) initialiser and properly size your layer in layoutSubviews.

CALayer live blur inside AVVideoCompositionCoreAnimationTool

I am trying to come up with a way to perform a live blur during AVVideoCompositionCoreAnimationTool export. I have tried UIVisualEffectView and stealing the layer of the underlying view. It works in preview but as soon as you use it inside AVVideoCompositionCoreAnimationTool the layer is black. So I started building a CALayer that does this but it is not updating often enough. What can I do to make it draw more often or what might work for using the AVVideoCompositionCoreAnimationTool and a live blur in iOS? Here is the layer I built.
class CABlurLayer : CALayer{
let maxBlurRadius : CGFloat = 20
var currentImageIndex : Float = 0
var blur : Int = 10
var context : CGContext?
var link : Timer?
var snap : UIImage?
var targetLayer : CALayer?
override init() {
super.init()
}
convenience init(targetLayer:CALayer?){
self.init()
self.targetLayer = targetLayer
self.drawsAsynchronously = true
if let tl = targetLayer{
self.masksToBounds = tl.masksToBounds
}
updateSnapShots()
link = Timer.scheduledTimer(timeInterval: 1/60, target: self, selector: #selector(updateBlur), userInfo: nil, repeats: true)
}
#objc func updateBlur(){
updateSnapShots()
DispatchQueue.main.async {
self.setNeedsDisplay()
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateSnapShots(){
guard let tl = targetLayer else{return}
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0)
guard let ctx = UIGraphicsGetCurrentContext() else{return}
tl.render(in: ctx)
let snapshot = UIGraphicsGetImageFromCurrentImageContext()
snap = snapshot?.applyBlurWithRadius(CGFloat(blur), tintColor: UIColor().withAlphaComponent(0), saturationDeltaFactor: 1.4)
}
override func draw(in ctx: CGContext) {
guard let blurredImage = snap,
let tl = targetLayer else{return}
var origin = tl.frame.origin
if let pres = tl.presentation(){
origin = pres.frame.origin
}
UIGraphicsPushContext(ctx)
blurredImage.draw(at: origin)
UIGraphicsPopContext()
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
let ur = URL(string: "https://images.pexels.com/photos/457882/pexels-photo-457882.jpeg?auto=compress&cs=tinysrgb&dpr=2&w=500")
URLSession.shared.dataTask(with: ur!) { (dt, response, error) in
if let data = dt{
print("we have a response")
let img = UIImage(data: data)
DispatchQueue.main.async {
let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
view.layer.addSublayer(layer)
let imageLayer = CALayer()
imageLayer.masksToBounds = true
imageLayer.frame = CGRect(x: 0, y: 150, width: 400, height: 300)
imageLayer.contentsGravity = .resizeAspectFill
imageLayer.contents = img?.cgImage
layer.addSublayer(imageLayer)
let blur = CABlurLayer(targetLayer: imageLayer)
blur.frame = layer.bounds
layer.addSublayer(blur)
blur.blur = 20
let pos = CABasicAnimation(keyPath: "position.x")
pos.toValue = imageLayer.position.x
pos.fromValue = imageLayer.position.x - 100
pos.duration = 2
pos.repeatCount = 100
pos.autoreverses = true
imageLayer.add(pos, forKey: nil)
}
}
}.resume()
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
PlaygroundPage.current.needsIndefiniteExecution = true
UIImage Extensions
import UIKit
import Accelerate
public extension UIImage {
public func applyLightEffect() -> UIImage? {
return applyBlurWithRadius(30, tintColor: UIColor(white: 1.0, alpha: 0.3), saturationDeltaFactor: 1.8)
}
public func applyExtraLightEffect() -> UIImage? {
return applyBlurWithRadius(20, tintColor: UIColor(white: 0.97, alpha: 0.82), saturationDeltaFactor: 1.8)
}
public func applyDarkEffect() -> UIImage? {
return applyBlurWithRadius(20, tintColor: UIColor(white: 0.11, alpha: 0.73), saturationDeltaFactor: 1.8)
}
public func applyTintEffectWithColor(_ tintColor: UIColor) -> UIImage? {
let effectColorAlpha: CGFloat = 0.6
var effectColor = tintColor
let componentCount = tintColor.cgColor.numberOfComponents
if componentCount == 2 {
var b: CGFloat = 0
if tintColor.getWhite(&b, alpha: nil) {
effectColor = UIColor(white: b, alpha: effectColorAlpha)
}
} else {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
if tintColor.getRed(&red, green: &green, blue: &blue, alpha: nil) {
effectColor = UIColor(red: red, green: green, blue: blue, alpha: effectColorAlpha)
}
}
return applyBlurWithRadius(10, tintColor: effectColor, saturationDeltaFactor: -1.0, maskImage: nil)
}
public func applyBlurWithRadius(_ blurRadius: CGFloat, tintColor: UIColor?, saturationDeltaFactor: CGFloat, maskImage: UIImage? = nil) -> UIImage? {
// Check pre-conditions.
if (size.width < 1 || size.height < 1) {
print("*** error: invalid size: \(size.width) x \(size.height). Both dimensions must be >= 1: \(self)")
return nil
}
guard let cgImage = self.cgImage else {
print("*** error: image must be backed by a CGImage: \(self)")
return nil
}
if maskImage != nil && maskImage!.cgImage == nil {
print("*** error: maskImage must be backed by a CGImage: \(String(describing: maskImage))")
return nil
}
let __FLT_EPSILON__ = CGFloat(Float.ulpOfOne)
let screenScale = UIScreen.main.scale
let imageRect = CGRect(origin: CGPoint.zero, size: size)
var effectImage = self
let hasBlur = blurRadius > __FLT_EPSILON__
let hasSaturationChange = fabs(saturationDeltaFactor - 1.0) > __FLT_EPSILON__
if hasBlur || hasSaturationChange {
func createEffectBuffer(_ context: CGContext) -> vImage_Buffer {
let data = context.data
let width = vImagePixelCount(context.width)
let height = vImagePixelCount(context.height)
let rowBytes = context.bytesPerRow
return vImage_Buffer(data: data, height: height, width: width, rowBytes: rowBytes)
}
UIGraphicsBeginImageContextWithOptions(size, false, screenScale)
guard let effectInContext = UIGraphicsGetCurrentContext() else { return nil }
effectInContext.scaleBy(x: 1.0, y: -1.0)
effectInContext.translateBy(x: 0, y: -size.height)
effectInContext.draw(cgImage, in: imageRect)
var effectInBuffer = createEffectBuffer(effectInContext)
UIGraphicsBeginImageContextWithOptions(size, false, screenScale)
guard let effectOutContext = UIGraphicsGetCurrentContext() else { return nil }
var effectOutBuffer = createEffectBuffer(effectOutContext)
if hasBlur {
// A description of how to compute the box kernel width from the Gaussian
// radius (aka standard deviation) appears in the SVG spec:
// http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement
//
// For larger values of 's' (s >= 2.0), an approximation can be used: Three
// successive box-blurs build a piece-wise quadratic convolution kernel, which
// approximates the Gaussian kernel to within roughly 3%.
//
// let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)
//
// ... if d is odd, use three box-blurs of size 'd', centered on the output pixel.
//
let inputRadius = blurRadius * screenScale
let d = floor(inputRadius * 3.0 * CGFloat(sqrt(2 * .pi) / 4 + 0.5))
var radius = UInt32(d)
if radius % 2 != 1 {
radius += 1 // force radius to be odd so that the three box-blur methodology works.
}
let imageEdgeExtendFlags = vImage_Flags(kvImageEdgeExtend)
vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, nil, 0, 0, radius, radius, nil, imageEdgeExtendFlags)
vImageBoxConvolve_ARGB8888(&effectOutBuffer, &effectInBuffer, nil, 0, 0, radius, radius, nil, imageEdgeExtendFlags)
vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, nil, 0, 0, radius, radius, nil, imageEdgeExtendFlags)
}
var effectImageBuffersAreSwapped = false
if hasSaturationChange {
let s: CGFloat = saturationDeltaFactor
let floatingPointSaturationMatrix: [CGFloat] = [
0.0722 + 0.9278 * s, 0.0722 - 0.0722 * s, 0.0722 - 0.0722 * s, 0,
0.7152 - 0.7152 * s, 0.7152 + 0.2848 * s, 0.7152 - 0.7152 * s, 0,
0.2126 - 0.2126 * s, 0.2126 - 0.2126 * s, 0.2126 + 0.7873 * s, 0,
0, 0, 0, 1
]
let divisor: CGFloat = 256
let matrixSize = floatingPointSaturationMatrix.count
var saturationMatrix = [Int16](repeating: 0, count: matrixSize)
for i: Int in 0 ..< matrixSize {
saturationMatrix[i] = Int16(round(floatingPointSaturationMatrix[i] * divisor))
}
if hasBlur {
vImageMatrixMultiply_ARGB8888(&effectOutBuffer, &effectInBuffer, saturationMatrix, Int32(divisor), nil, nil, vImage_Flags(kvImageNoFlags))
effectImageBuffersAreSwapped = true
} else {
vImageMatrixMultiply_ARGB8888(&effectInBuffer, &effectOutBuffer, saturationMatrix, Int32(divisor), nil, nil, vImage_Flags(kvImageNoFlags))
}
}
if !effectImageBuffersAreSwapped {
effectImage = UIGraphicsGetImageFromCurrentImageContext()!
}
UIGraphicsEndImageContext()
if effectImageBuffersAreSwapped {
effectImage = UIGraphicsGetImageFromCurrentImageContext()!
}
UIGraphicsEndImageContext()
}
// Set up output context.
UIGraphicsBeginImageContextWithOptions(size, false, screenScale)
guard let outputContext = UIGraphicsGetCurrentContext() else { return nil }
outputContext.scaleBy(x: 1.0, y: -1.0)
outputContext.translateBy(x: 0, y: -size.height)
// Draw base image.
outputContext.draw(cgImage, in: imageRect)
// Draw effect image.
if hasBlur {
outputContext.saveGState()
if let maskCGImage = maskImage?.cgImage {
outputContext.clip(to: imageRect, mask: maskCGImage);
}
outputContext.draw(effectImage.cgImage!, in: imageRect)
outputContext.restoreGState()
}
// Add in color tint.
if let color = tintColor {
outputContext.saveGState()
outputContext.setFillColor(color.cgColor)
outputContext.fill(imageRect)
outputContext.restoreGState()
}
// Output image is ready.
let outputImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return outputImage
}
public func blurImage()->UIImage?{
return self.applyBlurWithRadius(20, tintColor: UIColor().withAlphaComponent(0), saturationDeltaFactor: 1.4)
}
}
This has been around a while so I thought I would share my solution. I stole the CABackdropLayer from a UIVisualEffectView to achieve a live blur. You can init a layer of this type but it is private. However since a public view uses this layer and I am just taking it from that view I am not having to access a private api in a super direct way.

Don't understand how to fix Thread 1: ECX_BAD_ACCESS (code = EXC_I386_GPFLT) (line chart swift iOS)

I'm trying to make a line graph with no libraries, but I just cmd+c, cmd+v all the code. Yes, I know that I shouldn't do so, but I don't have much time
So I did everything with help of this - https://medium.com/#tstenerson/lets-make-a-line-chart-in-swift-3-5e819e6c1a00
Also added a view to the view controller and called it LineChart
But on line 42 I get an error Thread 1: ECX_BAD_ACCESS (code = EXC_I386_GPFLT)
lineChart.deltaX = 20
I don't know how to fix it
I coded only in ViewController.swift, here it is:
import UIKit
extension String {
func size(withSystemFontSize pointSize: CGFloat) -> CGSize {
return (self as NSString).size(attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: pointSize)])
}
}
extension CGPoint {
func adding(x: CGFloat) -> CGPoint { return CGPoint(x: self.x + x, y: self.y) }
func adding(y: CGFloat) -> CGPoint { return CGPoint(x: self.x, y: self.y + y) }
}
class ViewController: UIViewController {
#IBOutlet var lineChart: LineChart!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let f: (CGFloat) -> CGPoint = {
let noiseY = (CGFloat(arc4random_uniform(2)) * 2 - 1) * CGFloat(arc4random_uniform(4))
let noiseX = (CGFloat(arc4random_uniform(2)) * 2 - 1) * CGFloat(arc4random_uniform(4))
let b: CGFloat = 5
let y = 2 * $0 + b + noiseY
return CGPoint(x: $0 + noiseX, y: y)
}
let xs = [Int](1..<20)
let points = xs.map({f(CGFloat($0 * 10))})
lineChart.deltaX = 20
lineChart.deltaY = 30
lineChart.plot(points)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
class LineChart: UIView {
let lineLayer = CAShapeLayer()
let circlesLayer = CAShapeLayer()
var chartTransform: CGAffineTransform?
#IBInspectable var lineColor: UIColor = UIColor.green {
didSet {
lineLayer.strokeColor = lineColor.cgColor
}
}
#IBInspectable var lineWidth: CGFloat = 1
#IBInspectable var showPoints: Bool = true { // show the circles on each data point
didSet {
circlesLayer.isHidden = !showPoints
}
}
#IBInspectable var circleColor: UIColor = UIColor.green {
didSet {
circlesLayer.fillColor = circleColor.cgColor
}
}
#IBInspectable var circleSizeMultiplier: CGFloat = 3
#IBInspectable var axisColor: UIColor = UIColor.white
#IBInspectable var showInnerLines: Bool = true
#IBInspectable var labelFontSize: CGFloat = 10
var axisLineWidth: CGFloat = 1
var deltaX: CGFloat = 10 // The change between each tick on the x axis
var deltaY: CGFloat = 10 // and y axis
var xMax: CGFloat = 100
var yMax: CGFloat = 100
var xMin: CGFloat = 0
var yMin: CGFloat = 0
var data: [CGPoint]?
override init(frame: CGRect) {
super.init(frame: frame)
combinedInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
combinedInit()
}
func combinedInit() {
layer.addSublayer(lineLayer)
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = lineColor.cgColor
layer.addSublayer(circlesLayer)
circlesLayer.fillColor = circleColor.cgColor
layer.borderWidth = 1
layer.borderColor = axisColor.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
lineLayer.frame = bounds
circlesLayer.frame = bounds
if let d = data{
setTransform(minX: xMin, maxX: xMax, minY: yMin, maxY: yMax)
plot(d)
}
}
func setAxisRange(forPoints points: [CGPoint]) {
guard !points.isEmpty else { return }
let xs = points.map() { $0.x }
let ys = points.map() { $0.y }
// МИНИМАЛЬНЫЕ И МАКСИМАЛЬНЫЕ ЗНАЧЕНИЯ
xMax = ceil(xs.max()! / deltaX) * deltaX
yMax = ceil(ys.max()! / deltaY) * deltaY
xMin = 0
yMin = 0
setTransform(minX: xMin, maxX: xMax, minY: yMin, maxY: yMax)
}
func setAxisRange(xMin: CGFloat, xMax: CGFloat, yMin: CGFloat, yMax: CGFloat) {
self.xMin = xMin
self.xMax = xMax
self.yMin = yMin
self.yMax = yMax
setTransform(minX: xMin, maxX: xMax, minY: yMin, maxY: yMax)
}
func setTransform(minX: CGFloat, maxX: CGFloat, minY: CGFloat, maxY: CGFloat) {
let xLabelSize = "\(Int(maxX))".size(withSystemFontSize: labelFontSize)
let yLabelSize = "\(Int(maxY))".size(withSystemFontSize: labelFontSize)
let xOffset = xLabelSize.height + 2
let yOffset = yLabelSize.width + 5
let xScale = (bounds.width - yOffset - xLabelSize.width/2 - 2)/(maxX - minX)
let yScale = (bounds.height - xOffset - yLabelSize.height/2 - 2)/(maxY - minY)
chartTransform = CGAffineTransform(a: xScale, b: 0, c: 0, d: -yScale, tx: yOffset, ty: bounds.height - xOffset)
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
// draw rect comes with a drawing context, so lets grab it.
// Also, if there is not yet a chart transform, we will bail on performing any other drawing.
// I like guard statements for this because it's kind of like a bouncer to a bar.
// If you don't have your transform yet, you can't enter drawAxes.
guard let context = UIGraphicsGetCurrentContext(), let t = chartTransform else { return }
drawAxes(in: context, usingTransform: t)
}
func drawAxes(in context: CGContext, usingTransform t: CGAffineTransform) {
context.saveGState()
// Make two paths, one for thick lines, one for thin.
let thickerLines = CGMutablePath()
let thinnerLines = CGMutablePath()
// The two line chart axes.
let xAxisPoints = [CGPoint(x: xMin, y: 0), CGPoint(x: xMax, y: 0)]
let yAxisPoints = [CGPoint(x: 0, y: yMin), CGPoint(x: 0, y: yMax)]
// Add each to thicker lines but apply our transform too.
thickerLines.addLines(between: xAxisPoints, transform: t)
thickerLines.addLines(between: yAxisPoints, transform: t)
// Next we go from xMin to xMax by deltaX using stride
for x in stride(from: xMin, through: xMax, by: deltaX) {
// Tick points are the points for the ticks on each axis.
// We check showInnerLines first to see if we are drawing small ticks or full lines.
// Yip for new guys: `let a = someBool ? b : c` is called a ternary operator.
// In English it means "let a = b if somebool is true, or c if it is false."
let tickPoints = showInnerLines ?
[CGPoint(x: x, y: yMin).applying(t), CGPoint(x: x, y: yMax).applying(t)] :
[CGPoint(x: x, y: 0).applying(t), CGPoint(x: x, y: 0).applying(t).adding(y: -5)]
thinnerLines.addLines(between: tickPoints)
if x != xMin { // draw the tick label (it is too buy if you draw it at the origin for both x & y
let label = "\(Int(x))" as NSString // Int to get rid of the decimal, NSString to draw
let labelSize = "\(Int(x))".size(withSystemFontSize: labelFontSize)
let labelDrawPoint = CGPoint(x: x, y: 0).applying(t)
.adding(x: -labelSize.width/2)
.adding(y: 1)
label.draw(at: labelDrawPoint,
withAttributes:
[NSFontAttributeName: UIFont.systemFont(ofSize: labelFontSize),
NSForegroundColorAttributeName: axisColor])
}
}
// Repeat for y.
for y in stride(from: yMin, through: yMax, by: deltaY) {
let tickPoints = showInnerLines ?
[CGPoint(x: xMin, y: y).applying(t), CGPoint(x: xMax, y: y).applying(t)] :
[CGPoint(x: 0, y: y).applying(t), CGPoint(x: 0, y: y).applying(t).adding(x: 5)]
thinnerLines.addLines(between: tickPoints)
if y != yMin {
let label = "\(Int(y))" as NSString
let labelSize = "\(Int(y))".size(withSystemFontSize: labelFontSize)
let labelDrawPoint = CGPoint(x: 0, y: y).applying(t)
.adding(x: -labelSize.width - 1)
.adding(y: -labelSize.height/2)
label.draw(at: labelDrawPoint,
withAttributes:
[NSFontAttributeName: UIFont.systemFont(ofSize: labelFontSize),
NSForegroundColorAttributeName: axisColor])
}
}
// Finally set stroke color & line width then stroke thick lines, repeat for thin.
context.setStrokeColor(axisColor.cgColor)
context.setLineWidth(axisLineWidth)
context.addPath(thickerLines)
context.strokePath()
context.setStrokeColor(axisColor.withAlphaComponent(0.5).cgColor)
context.setLineWidth(axisLineWidth/2)
context.addPath(thinnerLines)
context.strokePath()
context.restoreGState()
// Whenever you change a graphics context you should save it prior and restore it after.
// If we were using a context other than draw(rect) we would have to also end the graphics context.
}
func plot(_ points: [CGPoint]) {
lineLayer.path = nil
circlesLayer.path = nil
data = nil
guard !points.isEmpty else { return }
self.data = points
if self.chartTransform == nil {
setAxisRange(forPoints: points)
}
let linePath = CGMutablePath()
linePath.addLines(between: points, transform: chartTransform!)
lineLayer.path = linePath
if showPoints {
circlesLayer.path = circles(atPoints: points, withTransform: chartTransform!)
}
}
func circles(atPoints points: [CGPoint], withTransform t: CGAffineTransform) -> CGPath {
let path = CGMutablePath()
let radius = lineLayer.lineWidth * circleSizeMultiplier/2
for i in points {
let p = i.applying(t)
let rect = CGRect(x: p.x - radius, y: p.y - radius, width: radius * 2, height: radius * 2)
path.addEllipse(in: rect)
}
return path
}
} // <- I didn't close the LineChart class up top, closing it now
}
In storyboard remove reference outlet link to 'lineChart' and try this:
import UIKit
extension String {
func size(withSystemFontSize pointSize: CGFloat) -> CGSize {
return (self as NSString).size(attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: pointSize)])
}
}
extension CGPoint {
func adding(x: CGFloat) -> CGPoint { return CGPoint(x: self.x + x, y: self.y) }
func adding(y: CGFloat) -> CGPoint { return CGPoint(x: self.x, y: self.y + y) }
}
class ViewController: UIViewController {
// #IBOutlet var lineChart: LineChart! ////////////REMOVED THIS
var lineChart = LineChart(frame: CGRect.zero) ////////////ADDED THIS
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let f: (CGFloat) -> CGPoint = {
let noiseY = (CGFloat(arc4random_uniform(2)) * 2 - 1) * CGFloat(arc4random_uniform(4))
let noiseX = (CGFloat(arc4random_uniform(2)) * 2 - 1) * CGFloat(arc4random_uniform(4))
let b: CGFloat = 5
let y = 2 * $0 + b + noiseY
return CGPoint(x: $0 + noiseX, y: y)
}
let xs = [Int](1..<20)
let points = xs.map({f(CGFloat($0 * 10))})
////////////ADDED THIS
self.lineChart.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)
self.view.addSubview(self.lineChart)
lineChart.deltaX = 20
lineChart.deltaY = 30
lineChart.plot(points)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
class LineChart: UIView {
let lineLayer = CAShapeLayer()
let circlesLayer = CAShapeLayer()
var chartTransform: CGAffineTransform?
#IBInspectable var lineColor: UIColor = UIColor.green {
didSet {
lineLayer.strokeColor = lineColor.cgColor
}
}
#IBInspectable var lineWidth: CGFloat = 1
#IBInspectable var showPoints: Bool = true { // show the circles on each data point
didSet {
circlesLayer.isHidden = !showPoints
}
}
#IBInspectable var circleColor: UIColor = UIColor.green {
didSet {
circlesLayer.fillColor = circleColor.cgColor
}
}
#IBInspectable var circleSizeMultiplier: CGFloat = 3
#IBInspectable var axisColor: UIColor = UIColor.white
#IBInspectable var showInnerLines: Bool = true
#IBInspectable var labelFontSize: CGFloat = 10
var axisLineWidth: CGFloat = 1
var deltaX: CGFloat = 10 // The change between each tick on the x axis
var deltaY: CGFloat = 10 // and y axis
var xMax: CGFloat = 100
var yMax: CGFloat = 100
var xMin: CGFloat = 0
var yMin: CGFloat = 0
var data: [CGPoint]?
override init(frame: CGRect) {
super.init(frame: frame)
combinedInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
combinedInit()
}
func combinedInit() {
layer.addSublayer(lineLayer)
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = lineColor.cgColor
layer.addSublayer(circlesLayer)
circlesLayer.fillColor = circleColor.cgColor
layer.borderWidth = 1
layer.borderColor = axisColor.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
lineLayer.frame = bounds
circlesLayer.frame = bounds
if let d = data{
setTransform(minX: xMin, maxX: xMax, minY: yMin, maxY: yMax)
plot(d)
}
}
func setAxisRange(forPoints points: [CGPoint]) {
guard !points.isEmpty else { return }
let xs = points.map() { $0.x }
let ys = points.map() { $0.y }
// МИНИМАЛЬНЫЕ И МАКСИМАЛЬНЫЕ ЗНАЧЕНИЯ
xMax = ceil(xs.max()! / deltaX) * deltaX
yMax = ceil(ys.max()! / deltaY) * deltaY
xMin = 0
yMin = 0
setTransform(minX: xMin, maxX: xMax, minY: yMin, maxY: yMax)
}
func setAxisRange(xMin: CGFloat, xMax: CGFloat, yMin: CGFloat, yMax: CGFloat) {
self.xMin = xMin
self.xMax = xMax
self.yMin = yMin
self.yMax = yMax
setTransform(minX: xMin, maxX: xMax, minY: yMin, maxY: yMax)
}
func setTransform(minX: CGFloat, maxX: CGFloat, minY: CGFloat, maxY: CGFloat) {
let xLabelSize = "\(Int(maxX))".size(withSystemFontSize: labelFontSize)
let yLabelSize = "\(Int(maxY))".size(withSystemFontSize: labelFontSize)
let xOffset = xLabelSize.height + 2
let yOffset = yLabelSize.width + 5
let xScale = (bounds.width - yOffset - xLabelSize.width/2 - 2)/(maxX - minX)
let yScale = (bounds.height - xOffset - yLabelSize.height/2 - 2)/(maxY - minY)
chartTransform = CGAffineTransform(a: xScale, b: 0, c: 0, d: -yScale, tx: yOffset, ty: bounds.height - xOffset)
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
// draw rect comes with a drawing context, so lets grab it.
// Also, if there is not yet a chart transform, we will bail on performing any other drawing.
// I like guard statements for this because it's kind of like a bouncer to a bar.
// If you don't have your transform yet, you can't enter drawAxes.
guard let context = UIGraphicsGetCurrentContext(), let t = chartTransform else { return }
drawAxes(in: context, usingTransform: t)
}
func drawAxes(in context: CGContext, usingTransform t: CGAffineTransform) {
context.saveGState()
// make two paths, one for thick lines, one for thin
let thickerLines = CGMutablePath()
let thinnerLines = CGMutablePath()
// the two line chart axes
let xAxisPoints = [CGPoint(x: xMin, y: 0), CGPoint(x: xMax, y: 0)]
let yAxisPoints = [CGPoint(x: 0, y: yMin), CGPoint(x: 0, y: yMax)]
// add each to thicker lines but apply our transform too.
thickerLines.addLines(between: xAxisPoints, transform: t)
thickerLines.addLines(between: yAxisPoints, transform: t)
// next we go from xMin to xMax by deltaX using stride
for x in stride(from: xMin, through: xMax, by: deltaX) {
// tick points are the points for the ticks on each axis
// we check showInnerLines first to see if we are drawing small ticks or full lines
// tip for new guys: `let a = someBool ? b : c` is called a ternary operator
// in english it means "let a = b if somebool is true, or c if it is false."
let tickPoints = showInnerLines ?
[CGPoint(x: x, y: yMin).applying(t), CGPoint(x: x, y: yMax).applying(t)] :
[CGPoint(x: x, y: 0).applying(t), CGPoint(x: x, y: 0).applying(t).adding(y: -5)]
thinnerLines.addLines(between: tickPoints)
if x != xMin { // draw the tick label (it is too buy if you draw it at the origin for both x & y
let label = "\(Int(x))" as NSString // Int to get rid of the decimal, NSString to draw
let labelSize = "\(Int(x))".size(withSystemFontSize: labelFontSize)
let labelDrawPoint = CGPoint(x: x, y: 0).applying(t)
.adding(x: -labelSize.width/2)
.adding(y: 1)
label.draw(at: labelDrawPoint,
withAttributes:
[NSFontAttributeName: UIFont.systemFont(ofSize: labelFontSize),
NSForegroundColorAttributeName: axisColor])
}
}
// repeat for y
for y in stride(from: yMin, through: yMax, by: deltaY) {
let tickPoints = showInnerLines ?
[CGPoint(x: xMin, y: y).applying(t), CGPoint(x: xMax, y: y).applying(t)] :
[CGPoint(x: 0, y: y).applying(t), CGPoint(x: 0, y: y).applying(t).adding(x: 5)]
thinnerLines.addLines(between: tickPoints)
if y != yMin {
let label = "\(Int(y))" as NSString
let labelSize = "\(Int(y))".size(withSystemFontSize: labelFontSize)
let labelDrawPoint = CGPoint(x: 0, y: y).applying(t)
.adding(x: -labelSize.width - 1)
.adding(y: -labelSize.height/2)
label.draw(at: labelDrawPoint,
withAttributes:
[NSFontAttributeName: UIFont.systemFont(ofSize: labelFontSize),
NSForegroundColorAttributeName: axisColor])
}
}
// finally set stroke color & line width then stroke thick lines, repeat for thin
context.setStrokeColor(axisColor.cgColor)
context.setLineWidth(axisLineWidth)
context.addPath(thickerLines)
context.strokePath()
context.setStrokeColor(axisColor.withAlphaComponent(0.5).cgColor)
context.setLineWidth(axisLineWidth/2)
context.addPath(thinnerLines)
context.strokePath()
context.restoreGState()
// whenever you change a graphics context you should save it prior and restore it after
// if we were using a context other than draw(rect) we would have to also end the graphics context
}
func plot(_ points: [CGPoint]) {
lineLayer.path = nil
circlesLayer.path = nil
data = nil
guard !points.isEmpty else { return }
self.data = points
if self.chartTransform == nil {
setAxisRange(forPoints: points)
}
let linePath = CGMutablePath()
linePath.addLines(between: points, transform: chartTransform!)
lineLayer.path = linePath
if showPoints {
circlesLayer.path = circles(atPoints: points, withTransform: chartTransform!)
}
}
func circles(atPoints points: [CGPoint], withTransform t: CGAffineTransform) -> CGPath {
let path = CGMutablePath()
let radius = lineLayer.lineWidth * circleSizeMultiplier/2
for i in points {
let p = i.applying(t)
let rect = CGRect(x: p.x - radius, y: p.y - radius, width: radius * 2, height: radius * 2)
path.addEllipse(in: rect)
}
return path
}
} // <- I didn't close the LineChart class up top, closing it now
}

Draw resizable rectangle using swift 3

How to draw resizable rectangle in UIView , i did many search on google and github and i found this one Click Here using swift 2.3 and i converted it to swift 3 .. but i can't resize rectangle after drawing it and thats the code
//
// ResizableRectangleView.swift
// DrawShapes
//
// Created by Jordan Focht on 3/9/15.
// Copyright (c) 2015 Jordan Focht. All rights reserved.
//
import Foundation
import UIKit
private let DefaultTint = UIColor(red: 0, green: 164 / 255.0, blue: 1.0, alpha: 1.0).cgColor
private let DefaultStrokeTint = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0).cgColor
private let ClearColor = UIColor.clear.cgColor
private let DefaultCircleRadius: CGFloat = 8
private let CornerTouchSize: CGFloat = 44
protocol ResizableRectangleViewDelegate : class {
func didSelectResizableRectangleView(_ view: ResizableRectangleView)
func didDeselectResizableRectangleView(_ view: ResizableRectangleView)
}
class ResizableRectangleView: UIControl {
fileprivate var borderLayer: CALayer = CALayer()
fileprivate var topLeftCircle = CALayer()
fileprivate var topRightCircle = CALayer()
fileprivate var bottomLeftCircle = CALayer()
fileprivate var bottomRightCircle = CALayer()
weak var delegate: ResizableRectangleViewDelegate?
var strokeTintColor: CGColor = DefaultStrokeTint
var circleRadius: CGFloat = DefaultCircleRadius
var nLocation : CGPoint!
override var frame: CGRect {
get {
return super.frame
}
set {
super.frame = newValue
self.updateLayers()
}
}
override var isSelected: Bool {
get {
return super.isSelected
}
set {
let changed = self.isSelected != newValue
super.isSelected = newValue
if changed {
if isSelected {
self.delegate?.didSelectResizableRectangleView(self)
} else {
self.delegate?.didDeselectResizableRectangleView(self)
}
}
}
}
func updateLayers() {
if self.layer.sublayers == nil {
self.layer.addSublayer(self.borderLayer)
self.layer.addSublayer(self.topLeftCircle)
self.layer.addSublayer(self.topRightCircle)
self.layer.addSublayer(self.bottomLeftCircle)
self.layer.addSublayer(self.bottomRightCircle)
let layers = (self.layer.sublayers ?? []) as [CALayer]
for layer in layers {
layer.contentsScale = UIScreen.main.scale
}
}
self.updateBorderLayer()
let circleFrame = self.borderLayer.frame
updateCircleLayer(topLeftCircle, center: CGPoint(x: circleFrame.origin.x, y: circleFrame.origin.y))
updateCircleLayer(topRightCircle, center: CGPoint(x: circleFrame.origin.x, y: circleFrame.maxY))
updateCircleLayer(bottomLeftCircle, center: CGPoint(x: circleFrame.maxX, y: circleFrame.origin.y))
updateCircleLayer(bottomRightCircle, center: CGPoint(x: circleFrame.maxX, y: circleFrame.maxY))
}
func borderedFrame() -> CGRect {
return self.borderLayer.frame
}
// var trackingFrameTransform: ((CGPoint) -> ())?
func moveFrame(_ originalFrame: CGRect, initialTouchLocation: CGPoint, _ location: CGPoint) {
let targetX = originalFrame.origin.x + location.x - initialTouchLocation.x
let targetY = originalFrame.origin.y + location.y - initialTouchLocation.y
let insetBounds = self.insetBounds()
self.frame.origin.x = max(insetBounds.origin.x, min(insetBounds.maxX - self.frame.width, targetX))
self.frame.origin.y = max(insetBounds.origin.y, min(insetBounds.maxY - self.frame.height, targetY))
nLocation = location
}
fileprivate func insetBounds() -> CGRect {
let inset = self.inset()
let contentBounds = (self.superview as? DrawableView)?.contentBounds ?? self.bounds
return contentBounds.insetBy(dx: -inset, dy: -inset)
}
func updateRect(_ anchor: CGPoint, initialTouchLocation: CGPoint, originalCorner: CGPoint , _ location: CGPoint) {
let insetBounds = self.insetBounds()
let locationX = max(insetBounds.origin.x, min(insetBounds.maxX, location.x))
let locationY = max(insetBounds.origin.y, min(insetBounds.maxY, location.y))
let targetX = originalCorner.x + locationX - initialTouchLocation.x
let targetY = originalCorner.y + locationY - initialTouchLocation.y
let minSize = self.inset() + circleRadius
if insetBounds.origin.x < targetX && targetX < insetBounds.maxX {
self.frame.origin.x = min(targetX, anchor.x)
self.frame.size.width = max(minSize * 2, abs(anchor.x - targetX))
}
if insetBounds.origin.y < targetY && targetY < insetBounds.maxY {
self.frame.origin.y = min(targetY, anchor.y)
self.frame.size.height = max(minSize * 2, abs(anchor.y - targetY))
}
nLocation = location
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
CATransaction.begin()
CATransaction.setDisableActions(true)
if let superview = self.superview as? DrawableView {
for view in superview.subviews {
if let view = view as? ResizableRectangleView {
if view != self {
view.isSelected = false
view.updateLayers()
}
}
}
superview.bringSubview(toFront: self)
}
let location = touch.location(in: self.superview)
nLocation = location
var anchor: CGPoint?
var corner: CGPoint?
switch (location.x, location.y) {
case (let x, let y) where x < self.frame.origin.x + CornerTouchSize && y < self.frame.origin.y + CornerTouchSize:
anchor = CGPoint(x: self.frame.maxX, y: self.frame.maxY)
corner = CGPoint(x: self.frame.minX, y: self.frame.minY)
case (let x, let y) where x < self.frame.origin.x + CornerTouchSize && y > self.frame.maxY - CornerTouchSize:
anchor = CGPoint(x: self.frame.maxX, y: self.frame.minY)
corner = CGPoint(x: self.frame.minX, y: self.frame.maxY)
case (let x, let y) where x > self.frame.maxX - CornerTouchSize && y < self.frame.origin.y + CornerTouchSize:
anchor = CGPoint(x: self.frame.minX, y: self.frame.maxY)
corner = CGPoint(x: self.frame.maxX, y: self.frame.minY)
case (let x, let y) where x > self.frame.maxX - CornerTouchSize && y > self.frame.maxY - CornerTouchSize:
anchor = CGPoint(x: self.frame.minX, y: self.frame.minY)
corner = CGPoint(x: self.frame.maxX, y: self.frame.maxY)
default:
self.moveFrame(self.frame, initialTouchLocation: location , nLocation)
}
if let anchor = anchor {
if let corner = corner {
self.didMove = true
self.isSelected = true
self.updateRect(anchor, initialTouchLocation: location, originalCorner: corner, nLocation)
self.updateLayers()
}
}
CATransaction.commit()
return true
}
var didMove = false
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
CATransaction.begin()
CATransaction.setDisableActions(true)
didMove = true
let location = touch.location(in: self.superview)
nLocation = location
//self.trackingFrameTransform?(location)
self.updateLayers()
CATransaction.commit()
return true
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
CATransaction.begin()
CATransaction.setDisableActions(true)
if !didMove {
self.isSelected = !self.isSelected
}
didMove = false
self.updateLayers()
// self.trackingFrameTransform = nil
nLocation = nil
CATransaction.commit()
}
func updateCircleLayer(_ layer: CALayer, center: CGPoint) {
layer.isHidden = !self.isSelected
layer.frame = CGRect(x: center.x - circleRadius, y: center.y - circleRadius, width: 2 * circleRadius, height: 2 * circleRadius)
layer.backgroundColor = self.tintColor.cgColor
layer.borderColor = strokeTintColor
layer.cornerRadius = self.circleRadius
layer.borderWidth = 1
layer.setNeedsDisplay()
}
func inset() -> CGFloat {
let circleInset = (CornerTouchSize - (self.circleRadius * 2)) / 2
return self.circleRadius + circleInset
}
func updateBorderLayer() {
self.borderLayer.masksToBounds = false
self.borderLayer.borderWidth = 1
self.borderLayer.borderColor = self.tintColor.cgColor
let inset = self.inset()
self.borderLayer.frame = self.bounds.insetBy(dx: inset, dy: inset)
self.borderLayer.setNeedsDisplay()
}
}
DrawableView.swift
import Foundation
import UIKit
struct ColoredRect {
let color: UIColor
let origin: CGPoint
let size: CGSize
var width: CGFloat {
get {
return self.size.width
}
}
var height: CGFloat {
get {
return self.size.height
}
}
}
class DrawableView: UIControl {
fileprivate let colorPicker = ColorPicker()
fileprivate var currentRect: ResizableRectangleView?
fileprivate var originalLocation: CGPoint?
fileprivate var rectIsPending = false
var contentSize: CGSize?
var contentBounds: CGRect? {
get {
if let contentSize = self.contentSize {
let scale = min(self.bounds.width / contentSize.width, self.bounds.height / contentSize.height)
let scaledWidth = contentSize.width * scale
let scaledHeight = contentSize.height * scale
let x = round(0.5 * (self.bounds.width - scaledWidth))
let y = round(0.5 * (self.bounds.height - scaledHeight))
return CGRect(x: x, y: y, width: scaledWidth, height: scaledHeight)
} else {
return nil
}
}
}
var shapes: [ColoredRect] {
get {
var shapes = [ColoredRect]()
for view in self.subviews {
if let view = view as? ResizableRectangleView {
let f = view.convert(view.borderedFrame(), to: self)
let relX = min(1.0, max(0.0, f.origin.x / self.bounds.width))
let relY = min(1.0, max(0.0, f.origin.y / self.bounds.height))
let relWidth = min(1.0, max(0.0, f.width / self.bounds.width))
let relHeight = min(1.0, max(0.0, f.height / self.bounds.height))
let relOrigin = CGPoint(x: relX, y: relY)
let relSize = CGSize(width: relWidth, height: relHeight)
let rect = ColoredRect(color: view.tintColor, origin: relOrigin, size: relSize)
shapes.append(rect)
}
}
return shapes
}
set {
let shapes = newValue
for view in self.subviews {
if let view = view as? ResizableRectangleView {
view.removeFromSuperview()
}
}
self.colorPicker.alpha = 0
for shape in shapes {
let x = shape.origin.x * self.bounds.width
let y = shape.origin.y * self.bounds.height
let width = shape.width * self.bounds.width
let height = shape.height * self.bounds.height
let rectFrame = CGRect(x: x, y: y, width: width, height: height)
let view = ResizableRectangleView()
let inset = view.inset()
view.tintColor = shape.color
view.frame = rectFrame.insetBy(dx: -inset, dy: -inset)
view.delegate = self
self.addSubview(view)
}
self.bringSubview(toFront: self.colorPicker)
}
}
// override init() {
// super.init()
// self.addColorPicker()
// }
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addColorPicker()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addColorPicker()
}
override func awakeFromNib() {
super.awakeFromNib()
self.addColorPicker()
}
fileprivate func addColorPicker() {
colorPicker.delegate = self
colorPicker.alpha = 0
self.addSubview(colorPicker)
self.bringSubview(toFront: self.colorPicker)
colorPicker.frame = CGRect(x: self.bounds.width - 44, y: 0, width: 44, height: self.bounds.height)
}
override func layoutSubviews() {
super.layoutSubviews()
colorPicker.frame = CGRect(x: self.bounds.width - 44, y: 0, width: 44, height: self.bounds.height)
}
override var canBecomeFirstResponder : Bool {
return true
}
override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
if (motion == UIEventSubtype.motionShake) {
self.shapes = []
}
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let location = touch.location(in: self)
if let contentBounds = self.contentBounds {
if (!contentBounds.contains(location)) {
return false
}
}
rectIsPending = true
let newRect = ResizableRectangleView()
newRect.frame = CGRect(x: location.x, y: location.y, width: 1, height: 1)
newRect.tintColor = UIColor(cgColor: self.colorPicker.color)
self.currentRect = newRect
self.originalLocation = location
CATransaction.begin()
CATransaction.setDisableActions(true)
for view in self.subviews {
if let view = view as? ResizableRectangleView {
view.isSelected = false
view.updateLayers()
}
}
CATransaction.commit()
return true
}
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
if let currentRect = self.currentRect {
if rectIsPending {
currentRect.delegate = self
self.addSubview(currentRect)
self.bringSubview(toFront: self.colorPicker)
}
CATransaction.begin()
CATransaction.setDisableActions(true)
if let originalLocation = self.originalLocation {
let location = touch.location(in: self)
currentRect.updateRect(originalLocation, initialTouchLocation: originalLocation, originalCorner: originalLocation, location)
// currentRect.updateRect(originalLocation, initialTouchLocation: originalLocation, originalCorner: originalLocation ,location: location)
}
CATransaction.commit()
}
return super.continueTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
self.currentRect = nil
self.rectIsPending = false
}
}
extension DrawableView: ColorPickerDelegate {
func colorPicker(_ picker: ColorPicker, didChangeColor color: CGColor) {
CATransaction.begin()
CATransaction.setDisableActions(true)
for view in self.subviews {
if let view = view as? ResizableRectangleView {
if view.isSelected {
view.tintColor = UIColor(cgColor: color)
view.updateLayers()
}
}
}
CATransaction.commit()
}
}
extension DrawableView: ResizableRectangleViewDelegate {
func didSelectResizableRectangleView(_ view: ResizableRectangleView) {
self.bringSubview(toFront: self.colorPicker)
if self.colorPicker.alpha == 0 {
UIView.animate(withDuration: 0.15, animations: {
self.colorPicker.alpha = 1
})
}
}
func didDeselectResizableRectangleView(_ view: ResizableRectangleView) {
self.bringSubview(toFront: self.colorPicker)
if colorPicker.alpha == 1 {
let selectionCount = self.subviews.reduce(0) {
acc, view in
if let view = view as? ResizableRectangleView {
return acc + (view.isSelected ? 1 : 0)
}
return acc
}
if selectionCount == 0 {
UIView.animate(withDuration: 0.15, animations: {
self.colorPicker.alpha = 0
})
}
}
}
}

Resources