iOS8 drawRect in Playground weird crashing - swift

I've got a nice bar graph class working in an app using my own Core Graphics code, and playing around with it in playground I notice that drawRect seems to be called an awful lot of times when nothing is changing (I've abbreviated my code below, but theres the gist).
I've noticed if I put the axis drawing code in a method, then call that exactly where the code used to be, the playground runs drawRect thousands of times until it crashes the program. If I have it how it is shown below, it runs only once.
class BarGraphView: UIView {
var frameWidth = CGFloat(0.0)
var frameHeight = CGFloat(0.0)
var values = [CGFloat]()
var maxY = CGFloat(0.0)
var title = NSString(string: "")
var xLabel = NSString(string: "")
let gridOffSetY = CGFloat(0.1)
var xaxis : CGFloat {
return 0.6*frameWidth
}
var yaxis : CGFloat {
return 0.6*frameWidth
}
var xstart : CGFloat {
return 0.2*frameWidth
}
var ystart : CGFloat {
return 0.3*frameWidth
}
var xseparation: CGFloat {
return CGFloat(xaxis*0.05)
}
init(frame: CGRect, data: [CGFloat], maxYVal: Int, title: String, xLabel: String) {
super.init(frame: frame)
frameWidth = frame.width
frameHeight = frame.height
values = data
maxY = CGFloat(maxYVal)
self.title = title as NSString
self.xLabel = xLabel as NSString
}
override func drawRect(rect: CGRect) {
let numValues = CGFloat(values.count)
let context = UIGraphicsGetCurrentContext()
CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor);
CGContextFillRect(context, bounds);
CGContextSetFillColorWithColor(context, UIColor.greenColor().CGColor)
let barWidth:CGFloat = ((xaxis-(numValues+1)*xseparation)/numValues)
var count:CGFloat = 0
for number in values {
let x = xstart + xseparation + (count * (barWidth + xseparation))
let barRect = CGRect(x: x, y:ystart+yaxis, width: barWidth, height: -(number/maxY)*yaxis)
CGContextAddRect(context, barRect)
count++
}
CGContextFillPath(context)
// IF THIS CODE IS HERE, IT IS RUN ONCE IN PLAYGROUND
// Axis Drawing
var bezier = UIBezierPath()
// Draw y-axis
bezier.moveToPoint(CGPointMake(xstart,ystart))
bezier.addLineToPoint(CGPointMake(xstart,ystart+yaxis+5))
// Draw x-axis
bezier.moveToPoint(CGPointMake(xstart-5,ystart+yaxis))
bezier.addLineToPoint(CGPointMake(xstart+xaxis,ystart+yaxis))
// Draw value markings on y-axis
let divisor = CGFloat((yaxis-gridOffSetY)/maxY) // max y-value, with offset
for i in 1..<maxY {
bezier.moveToPoint(CGPointMake(xstart-CGFloat(2.5),ystart+gridOffSetY+CGFloat(i)*divisor))
bezier.addLineToPoint(CGPointMake(xstart,ystart+gridOffSetY+CGFloat(i)*divisor))
}
UIColor.blackColor().setStroke()
bezier.stroke()
// IF PUT THAT ^ in this method, it runs thousands of times consecutively, crashing Xcode.
//drawAxis()
}
Does this look like potentially buggy behaviour, is playground just being weird, or is there any logical explanation for this?

Related

Drawing a Tiled Logo over NSImage an 45 degree

I'm trying to draw a logo tiled over an image at 45 degrees.But I always get a spacing on the left side.
var y_offset: CGFloat = logo.size.width * sin(45 * (CGFloat.pi / 180.0))
// the sin of the angle may return zero or negative value,
// it won't work with this formula
if y_offset >= 0 {
var x: CGFloat = 0
while x < size.width {
var y: CGFloat = 0
while y < size.height {
// move to this position
context.saveGState()
context.translateBy(x: x, y: y)
// draw text rotated around its center
context.rotate(by: ((CGFloat(-45) * CGFloat.pi ) / 180))
logo.draw(at:NSPoint(x:x,y:y), from: .zero, operation: .sourceOver, fraction: CGFloat(logotransparency))
// reset
context.restoreGState()
y = y + CGFloat(y_offset)
}
x = x + logo.size.width
}}
}
This is the result what I get.
As you can see there are some spacing present on the left side.I cannot figure out what I'm doing wrong.I have tried setting y to size.height and decrementing it by y_offset in the loop.But I get the same result.
Update:
var dirtyRect:NSRect=NSMakeRect(0, 0, size.width, size.height)
let deg45 = CGFloat.pi / 4
if let ciImage = logo.ciImage {
let ciTiled = ciImage.tiled(at: deg45).cropped(to: dirtyRect)
let color = NSColor.init(patternImage: NSImage.fromCIImage(ciTiled))
color.setFill()
context.fill(dirtyRect)
}
Updated answer
If you need more control over the appearance you can go with manually drawing the overlays. See below code for a fixed version of your original code with two options for spacing.
In production, you would of course want to avoid using ! and move the image loading out of the draw function (even though NSImage(named:) uses a cache).
override func draw(_ dirtyRect: NSRect) {
let bgImage = NSImage(named: "landscape")!
bgImage.draw(in: dirtyRect)
let deg45 = CGFloat.pi / 4
let logo = NSImage(named: "TextTile")!
let context = NSGraphicsContext.current!.cgContext
let h = logo.size.height // (sin(deg45) * logo.size.height) + (cos(deg45) * logo.size.height)
let w = logo.size.width // (sin(deg45) * logo.size.width ) + (cos(deg45) * logo.size.width )
var x: CGFloat = -w
while x < dirtyRect.width + w {
var y: CGFloat = -h
while y < dirtyRect.height + h {
context.saveGState()
context.translateBy(x: x, y: y)
context.rotate(by: deg45)
logo.draw(at:NSPoint(x:0,y:0),
from: .zero,
operation: .sourceOver,
fraction: 1)
context.restoreGState()
y = y + h
}
x = x + w
}
super.draw(dirtyRect)
}
Original answer
You can set a backgroundColor with a patternImage to for the effect of drawing image tiles in a rect.
To tilt the image by some angle, use CIImage's CIAffineTile option with some transformation.
Here is some example code:
import Cocoa
import CoreImage
class ViewController: NSViewController {
override func loadView() {
let size = CGSize(width: 500, height: 500)
let view = TiledView(frame: CGRect(origin: CGPointZero, size: size))
self.view = view
}
}
class TiledView: NSView {
override func draw(_ dirtyRect: NSRect) {
let bgImage = NSImage(named: "landscape")!
bgImage.draw(in: dirtyRect)
let deg45 = CGFloat.pi / 4
if let ciImage = NSImage(named: "TextTile")?.ciImage() {
let ciTiled = ciImage.tiled(at: deg45).cropped(to: dirtyRect)
let color = NSColor.init(patternImage: NSImage.fromCIImage(ciTiled))
color.setFill()
dirtyRect.fill()
}
super.draw(dirtyRect)
}
}
extension NSImage {
// source: https://rethunk.medium.com/convert-between-nsimage-and-ciimage-in-swift-d6c6180ef026
func ciImage() -> CIImage? {
guard let data = self.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: data) else {
return nil
}
let ci = CIImage(bitmapImageRep: bitmap)
return ci
}
static func fromCIImage(_ ciImage: CIImage) -> NSImage {
let rep = NSCIImageRep(ciImage: ciImage)
let nsImage = NSImage(size: rep.size)
nsImage.addRepresentation(rep)
return nsImage
}
}
extension CIImage {
func tiled(at angle: CGFloat) -> CIImage {
// try different transforms here
let transform = CGAffineTransform(rotationAngle: angle)
return self.applyingFilter("CIAffineTile", parameters: [kCIInputTransformKey: transform])
}
}
The result looks like this:

setColor on NSBitmapImageRep not working in Swift

I'm trying to figure out how setColor works. I have the following code:
lazy var imageView:NSImageView = {
let imageView = NSImageView(frame: view.frame)
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
createColorProjection()
view.wantsLayer = true
view.addSubview(imageView)
view.needsDisplay = true
}
func createColorProjection() {
var bitmap = NSBitmapImageRep(cgImage: cgImage!)
var x = 0
while x < bitmap.pixelsWide {
var y = 0
while y < bitmap.pixelsHigh {
//pixels[Point(x: x, y: y)] = (getColor(x: x, y: y, bitmap: bitmap))
bitmap.setColor(NSColor(cgColor: .black)!, atX: x, y: y)
y += 1
}
x += 1
}
let image = createImage(bitmap: bitmap)
imageView.image = image
imageView.needsDisplay = true
}
func createImage(bitmap:NSBitmapImageRep) -> NSImage {
let image = bitmap.cgImage
return NSImage(cgImage: image! , size: CGSize(width: image!.width, height: image!.height))
}
The intention of the code is to change a photo (a rainbow) to be entirely black (I'm just testing with black right now to make sure I understand how it works). However, when I run the program, the unchanged picture of the rainbow is shown, not a black photo.
I am getting these errors:
Unrecognized colorspace number -1 and Unknown number of components for colorspace model -1.
Thanks.
First, you're right: setColor has been broken at least since Catalina. Apple hasn't fixed it probably because it's so slow and inefficient, and nobody ever used it.
Second, docs say NSBitmapImageRep(cgImage: CGImage) produces a read-only bitmap so your code wouldn't have worked even if setColor worked.
As Alexander says, making your own CIFilter is the best way to change a photo's pixels to different colors. Writing and implementing the OpenGL isn't easy, but it's the best.
If you were to add an extension to NSBitmapImageRep like this:
extension NSBitmapImageRep {
func setColorNew(_ color: NSColor, atX x: Int, y: Int) {
guard let data = bitmapData else { return }
let ptr = data + bytesPerRow * y + samplesPerPixel * x
ptr[0] = UInt8(color.redComponent * 255.1)
ptr[1] = UInt8(color.greenComponent * 255.1)
ptr[2] = UInt8(color.blueComponent * 255.1)
if samplesPerPixel > 3 {
ptr[3] = UInt8(color.alphaComponent * 255.1)
}
}
}
Then simply changing an image's pixels could be done like this:
func changePixels(image: NSImage, newColor: NSColor) -> NSImage {
guard let imgData = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: imgData),
let color = newColor.usingColorSpace(.deviceRGB)
else { return image }
var y = 0
while y < bitmap.pixelsHigh {
var x = 0
while x < bitmap.pixelsWide {
bitmap.setColorNew(color, atX: x, y: y)
x += 1
}
y += 1
}
let newImage = NSImage(size: image.size)
newImage.addRepresentation(bitmap)
return newImage
}

How do I render CALayer sublayers in the correct position

I am trying to get an image drawn of a CALayer containing a number of sublayers positioned at specific points, but at the moment it does not honour the zPosition of the sublayers when I use CALayer.render(in ctx:). It works fine on screen but when rendering to PDF it seems to render them in the order they were created.
These sublayers that are positioned(x, y, angle) on the drawing layer.
One solution seems to be to override the render(in ctx:) method on the drawing layer, which seems to work except the rendering of the sublayers is in the incorrect position. They are all in the bottom left corner (0,0) and not rotated correctly.
override func render(in ctx: CGContext) {
if let layers:[CALayer] = self.sublayers {
let orderedLayers = layers.sorted(by: {
$0.zPosition < $1.zPosition
})
for v in orderedLayers {
v.render(in: ctx)
}
}
}
If I don't override this method then they are positioned correctly but just in the wrong zPosition - i.e. ones that should be at the bottom (zPosition-0) are at the top.
What am I missing here ? It seems I need to position the sublayers correctly somehow in the render(incts:) function?
How do I do this ? These sublayers have already been positioned on screen and all I am trying to do is generate an image of the drawing. This is done using the following function.
func createPdfData()->Data?{
DebugLog("")
let scale: CGFloat = 1
let mWidth = drawingLayer.frame.width * scale
let mHeight = drawingLayer.frame.height * scale
var cgRect = CGRect(x: 0, y: 0, width: mWidth, height: mHeight)
let documentInfo = [kCGPDFContextCreator as String:"MakeSpace(www.xxxx.com)",
kCGPDFContextTitle as String:"Layout Image",
kCGPDFContextAuthor as String:GlobalVars.shared.appUser?.username ?? "",
kCGPDFContextSubject as String:self.level?.imageCode ?? "",
kCGPDFContextKeywords as String:"XXXX, Layout"]
let data = NSMutableData()
guard let pdfData = CGDataConsumer(data: data),
let ctx = CGContext.init(consumer: pdfData, mediaBox: &cgRect, documentInfo as CFDictionary) else {
return nil}
ctx.beginPDFPage(nil)
ctx.saveGState()
ctx.scaleBy(x: scale, y: scale)
self.drawingLayer.render(in: ctx)
ctx.restoreGState()
ctx.endPDFPage()
ctx.closePDF()
return data as Data
}
This is what I ended up doing - and it seems to work.
class ZOrderDrawingLayer: CALayer {
override func render(in ctx: CGContext) {
if let layers:[CALayer] = self.sublayers {
let orderedLayers = layers.sorted(by: {
$0.zPosition < $1.zPosition
})
for v in orderedLayers {
ctx.saveGState()
// Translate and rotate the context using the sublayers
// size, position and transform (angle)
let w = v.bounds.width/2
let ww = w*w
let h = v.bounds.height/2
let hh = h*h
let c = sqrt(ww + hh)
let theta = asin(h/c)
let angle = atan2(v.transform.m12, v.transform.m11)
let x = c * cos(theta+angle)
let y = c * sin(theta+angle)
ctx.translateBy(x: v.position.x-x, y: v.position.y-y)
ctx.rotate(by: angle)
v.render(in: ctx)
ctx.restoreGState()
}
}
}
}

How can I animate the properties of my custom UIView?

I frequently need to round only two corners in a view, and sometimes need to use gradients. I've found that the common solution of using a CALayerMask is detrimental to performance, so I devised my own solution overriding drawRect(rect: CGRect). It works well, providing an easy way to round some or all corners, draw a border, and use both linear and radial gradient fills, even being able to set color stops for the gradients.
Unfortunately, when I try to animate these properties with UIView.animateWithDuration, my corners, gradients, and borders don't animate. Rather, they look "stretched" in the initial state, then animate to the final state. I've read that this can be solved with CALayer animation, but I'm not quite clear on the nature of the problem. Is there a way I can solve this as the class is now? If not, when is drawRect(rect: CGRect) preferable to drawLayer(layer: CALayer, inContext ctx: CGContext)?
I'm also open to general suggestions on improving this class.
AppocalypseUI.swift (provides support functions for UI operations)
//
// AppocalypseUI.swift
// Soapbox
//
// Created by Joseph Falcone on 6/2/16.
// Copyright © 2016 Joseph Falcone. All rights reserved.
//
import UIKit
class AppocalypseUI: NSObject
{
/// Generates an array of CGFloat values ranging from 0.0-1.0 which represent the color stops in a gradient
class func makeLinearColorStops(numStops:Int) -> [CGFloat]
{
assert(numStops >= 2, "Must have at least two color stops.")
let stepIncrement = 1.0/Double(numStops-1)
var returnArr : [CGFloat] = []
// The first stop is always 0
returnArr += [0.0]
for i in 1 ..< numStops-1
{
let stepVal = stepIncrement*Double(i)
let stepFactor = CGFloat(fmod(stepVal, 1.0))
returnArr += [stepFactor]
}
// The last stop is always 1
returnArr += [1.0]
// Fini
return returnArr
}
/// Returns the stop colors in an array
class func colorsAlongArray(colorArr:[UIColor], steps:Int) -> [UIColor]
{
let arrCount = colorArr.count
let stepIncrement = Double(arrCount)/Double(steps)
var returnArr : [UIColor] = []
for i in 0..<steps
{
let stepVal = stepIncrement*Double(i)
let stepFactor = CGFloat(fmod(stepVal, 1.0))
let stepIndex1 = Int(floor(stepVal/1.0))
var stepIndex2 = Int(ceil(stepVal/1.0))
if(stepIndex2 > arrCount-1)
{stepIndex2 = arrCount-1}
let color1 = colorArr[stepIndex1]
let color2 = colorArr[stepIndex2]
let color = colorByInterpolatingColors(color1, color2: color2, factor: stepFactor)
returnArr += [color]
}
return returnArr
}
/// Returns a color between two colors on a gradient
class func colorByInterpolatingColors(color1:UIColor, color2:UIColor, factor:CGFloat) -> UIColor
{
let startComponent = CGColorGetComponents(color1.CGColor)
let endComponent = CGColorGetComponents(color2.CGColor)
let startAlpha = CGColorGetAlpha(color1.CGColor)
let endAlpha = CGColorGetAlpha(color2.CGColor)
let r = startComponent[0] + (endComponent[0] - startComponent[0]) * factor
let g = startComponent[1] + (endComponent[1] - startComponent[1]) * factor
let b = startComponent[2] + (endComponent[2] - startComponent[2]) * factor
let a = startAlpha + (endAlpha - startAlpha) * factor
return UIColor(red: r, green: g, blue: b, alpha: a)
}
/* No longer needed
class func getFloatArrayFromNSNumbers(numbers:[NSNumber]) -> [CGFloat]
{
var returnArr : [CGFloat] = []
for number in numbers
{
returnArr += [CGFloat(number.floatValue)]
}
return returnArr
}
*/
/// Returns an array containing the RGBA components of an array of colors
class func getFloatArrayFromUIColors(colors:[UIColor]) -> [CGFloat]
{
var returnArr : [CGFloat] = []
for color : UIColor in colors
{
var red : CGFloat = 0.0
var green : CGFloat = 0.0
var blue : CGFloat = 0.0
var alpha : CGFloat = 0.0
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
/*
// This check and backup should probably be implemented later, but it seems to fail when it shouldn't...probably improper use of optionals
if(color?.getRed(&red, green: &green, blue: &blue , alpha: &alpha) == nil)
{
// If for some reason the above function call fails, try this method of getting RGBA instead
let components = CGColorGetComponents(color?.CGColor)
red = components[0]
green = components[1]
blue = components[2]
alpha = components[3]
}
*/
returnArr += [red, green, blue, alpha]
}
return returnArr
}
/// Returns a path for a rectangle with rounded corners
class func newPathForRoundedRect(rect:CGRect, radiusTL radTL:CGFloat, radiusTR radTR:CGFloat, radiusBL radBL:CGFloat, radiusBR radBR:CGFloat, edges:UIRectEdge = .All) -> CGPathRef
{
let retPath = CGPathCreateMutable()
// Convenience
let rectL = rect.origin.x
let rectR = rect.origin.x+rect.size.width
let rectT = rect.origin.y
let rectB = rect.origin.y+rect.size.height
// Starting from the top left arc, move clockwise
let p1 = CGPointMake(rectL , rectT+radTL)
let p2 = CGPointMake(rectL+radTL, rectT)
let p3 = CGPointMake(rectR-radTR, rectT)
let p4 = CGPointMake(rectR , rectT+radTR)
let p5 = CGPointMake(rectR , rectB-radBR)
let p6 = CGPointMake(rectR-radBR, rectB)
let p7 = CGPointMake(rectL+radBL, rectB)
let p8 = CGPointMake(rectL , rectB-radBL)
let c1 = CGPointMake(rect.origin.x , rect.origin.y)
let c2 = CGPointMake(rect.origin.x+rect.size.width , rect.origin.y)
let c3 = CGPointMake(rect.origin.x+rect.size.width , rect.origin.y+rect.size.height)
let c4 = CGPointMake(rect.origin.x , rect.origin.y+rect.size.height)
if(edges.contains(.All) || (edges.contains(.Left) && edges.contains(.Right) && edges.contains(.Top) && edges.contains(.Bottom)))
{
CGPathMoveToPoint(retPath, nil, p1.x, p1.y)
CGPathAddArcToPoint (retPath, nil, c1.x, c1.y, p2.x, p2.y, radTL)
CGPathAddLineToPoint(retPath, nil, p3.x, p3.y)
CGPathAddArcToPoint (retPath, nil, c2.x, c2.y, p4.x, p4.y, radTR)
CGPathAddLineToPoint(retPath, nil, p5.x, p5.y)
CGPathAddArcToPoint (retPath, nil, c3.x, c3.y, p6.x, p6.y, radBR)
CGPathAddLineToPoint(retPath, nil, p7.x, p7.y)
CGPathAddArcToPoint (retPath, nil, c4.x, c4.y, p8.x, p8.y, radBL)
CGPathAddLineToPoint(retPath, nil, p1.x, p1.y)
CGPathCloseSubpath(retPath)
return retPath
}
if(edges.contains(.Top))
{
CGPathMoveToPoint(retPath, nil, p1.x, p1.y)
CGPathAddArcToPoint (retPath, nil, c1.x, c1.y, p2.x, p2.y, radTL)
CGPathAddLineToPoint(retPath, nil, p3.x, p3.y)
CGPathAddArcToPoint (retPath, nil, c2.x, c2.y, p4.x, p4.y, radTR)
}
if(edges.contains(.Right))
{
CGPathMoveToPoint(retPath, nil, p3.x, p3.y)
CGPathAddArcToPoint (retPath, nil, c2.x, c2.y, p4.x, p4.y, radTR)
CGPathAddLineToPoint(retPath, nil, p5.x, p5.y)
CGPathAddArcToPoint (retPath, nil, c3.x, c3.y, p6.x, p6.y, radBR)
}
if(edges.contains(.Bottom))
{
CGPathMoveToPoint(retPath, nil, p5.x, p5.y)
CGPathAddArcToPoint (retPath, nil, c3.x, c3.y, p6.x, p6.y, radBR)
CGPathAddLineToPoint(retPath, nil, p7.x, p7.y)
CGPathAddArcToPoint (retPath, nil, c4.x, c4.y, p8.x, p8.y, radBL)
}
if(edges.contains(.Left))
{
CGPathMoveToPoint(retPath, nil, p7.x, p7.y)
CGPathAddArcToPoint (retPath, nil, c4.x, c4.y, p8.x, p8.y, radBL)
CGPathAddLineToPoint(retPath, nil, p1.x, p1.y)
CGPathAddArcToPoint (retPath, nil, c1.x, c1.y, p2.x, p2.y, radTL)
}
return retPath
}
}
JFStylishView.swift
//
// JFStylishView.swift
// Soapbox
//
// Created by Joseph Falcone on 6/2/16.
// Copyright © 2016 Joseph Falcone. All rights reserved.
//
import UIKit
enum GradientType
{
case Linear
case Radial
}
private enum BackgroundFillType
{
case Solid
case Gradient
}
class JFStylishView : UIView
{
// Rounded Corners
var cornerTL : CGFloat = 0.0
var cornerTR : CGFloat = 0.0
var cornerBR : CGFloat = 0.0
var cornerBL : CGFloat = 0.0
// Border
var borderWidth : CGFloat = 4.0
var borderColor = UIColor.greenColor()
// Colors
private var trueBackgroundColor = UIColor.clearColor() // The backgroundColor property has to be clear so that the layer doesn't draw behind the clipping area, so we use this to track what the user wants
private var bgColors : [CGFloat] = [] // array of colors used in drawrect
// Gradient points
private var gradientStart = CGPointMake(0.5, 0.0)
private var gradientEnd = CGPointMake(0.5, 1.0)
private var gradientColorStops : [CGFloat] = []
// Gradient type
private var gradientType : GradientType = .Linear
// Background Mode
private var backgroundFillType : BackgroundFillType = .Solid
// var shadowLayer: CAShapeLayer! // Not ready for this yet
// MARK: Initialization
override init(frame: CGRect)
{
super.init(frame:frame)
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder:aDecoder)
}
override func awakeFromNib()
{
super.awakeFromNib()
}
func initStylishStuff()
{
cornerTL = 0.0
}
// MARK: Color
private func getFillType() -> BackgroundFillType
{
// Rather than keeping a variable for this that gets set everywhere, we'll just use this getter to figure out what type we are using.
// Of course, if I get sloppy and don't make the unused elements empty when setting another fill parameter, this could produce a bug.
// RULES
// If using a gradient, trueBackgroundColor will be clear
// If using solid, bgColors will be empty
// If patterns are ever added, the above will be empty
if(bgColors.count == 0)
{return .Solid}
if(trueBackgroundColor == UIColor.clearColor())
{return .Gradient}
// Default
return .Solid
}
override var backgroundColor: UIColor?
{
get
{
return trueBackgroundColor
}
set
{
trueBackgroundColor = backgroundColor!
super.backgroundColor = UIColor.clearColor()
//bgColorArr = []
bgColors = []
backgroundFillType = .Solid
}
/*
// Property observer - whenever the background color is
didSet
{
bgColorArr = []
bgColors = []
// bgColorArr = [backgroundColor!]
// bgColors = AppocalypseUI.getFloatArrayFromUIColors([backgroundColor!, backgroundColor!])
}
*/
}
// // Convenient...maybe we shouldn't include this?
// func setBackgroundGradient(topColor:UIColor, bottomColor:UIColor)
// {
// bgColorArr = [topColor, bottomColor]
// bgColors = AppocalypseUI.getFloatArrayFromUIColors([topColor, bottomColor])
// }
// Default is linear, top to bottom
// startPoint, endPoint should be coordinates of 0.0-1.0
func setBackgroundGradient(colors:[UIColor], stops:[CGFloat]? = nil, startPoint:CGPoint?=nil, endPoint:CGPoint?=nil, type:GradientType = .Linear)
{
assert(colors.count > 1, "At least two colors must be specified.")
// We won't be using the backgroundColor property when drawing a gradient
trueBackgroundColor = UIColor.clearColor()
// Calculate the stops if they were not specified
var stops = stops // arguments are immutable, but we can declare a variable with the same name
if(stops == nil)
{
stops = AppocalypseUI.makeLinearColorStops(colors.count)
}
// Provide default start and end points if necessary
gradientType = type
switch type
{
case .Linear: // top to bottom
gradientStart = startPoint == nil ? CGPointZero : startPoint!
gradientEnd = endPoint == nil ? CGPointMake(0, 1.0) : endPoint!
case .Radial: // center to top
gradientStart = startPoint == nil ? CGPointMake(0.5, 0.5) : startPoint!
gradientEnd = endPoint == nil ? CGPointMake(0.5, 0) : endPoint!
}
assert(colors.count == stops?.count, "The number of colors and stops must be equal.")
//bgColorArr = colors
bgColors = AppocalypseUI.getFloatArrayFromUIColors(colors)
gradientColorStops = stops!
}
/*
override func layoutSubviews()
{
super.layoutSubviews()
if shadowLayer == nil
{
shadowLayer = CAShapeLayer()
shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 12).CGPath
//shadowLayer.fillColor = UIColor.whiteColor().CGColor
shadowLayer.fillColor = UIColor.clearColor().CGColor
shadowLayer.shadowColor = UIColor.darkGrayColor().CGColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowOffset = CGSize(width: 2.0, height: 2.0)
shadowLayer.shadowOpacity = 0.8
shadowLayer.shadowRadius = 2
//layer.insertSublayer(shadowLayer, atIndex: 0)
layer.insertSublayer(shadowLayer, below: nil) // also works
}
}
*/
// MARK: Drawing
// override func drawLayer(layer: CALayer, inContext ctx: CGContext) {
//
// }
override func drawRect(rect: CGRect)
{
// Get the current context
let context = UIGraphicsGetCurrentContext()
// Make the background gradient
let baseSpace = CGColorSpaceCreateDeviceRGB();
let gradient = CGGradientCreateWithColorComponents(baseSpace, bgColors, gradientColorStops, gradientColorStops.count);
// Set the border color and stroke
CGContextSetLineWidth(context, borderWidth);
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
// Fill in the background, inset by the border
let bgRect = CGRectMake(bounds.origin.x+borderWidth , bounds.origin.y+borderWidth , bounds.size.width-borderWidth*2, bounds.size.height-borderWidth*2);
let borderRect = CGRectMake(bounds.origin.x+borderWidth/2, bounds.origin.y+borderWidth/2, bounds.size.width-borderWidth , bounds.size.height-borderWidth);
let bgPath = AppocalypseUI.newPathForRoundedRect(bgRect, radiusTL: cornerTL, radiusTR: cornerTR, radiusBL: cornerBL, radiusBR: cornerBR)
let borderPath = AppocalypseUI.newPathForRoundedRect(borderRect, radiusTL: cornerTL, radiusTR: cornerTR, radiusBL: cornerBL, radiusBR: cornerBR)
CGContextStrokePath(context)
// Background
CGContextSaveGState(context); // Saves the state from before we clipped to the path
CGContextAddPath(context, bgPath);
CGContextClip(context); // Makes the background fill only the path
switch getFillType()
{
case .Gradient:
let gradientStartInPoints = CGPointMake(gradientStart.x*bounds.size.width, gradientStart.y*bounds.size.height);
let gradientEndInPoints = CGPointMake(gradientEnd.x*bounds.size.width, gradientEnd.y*bounds.size.height);
switch(gradientType)
{
case .Linear:
CGContextDrawLinearGradient(context, gradient, gradientStartInPoints, gradientEndInPoints, []); // Draw a vertical gradient
case .Radial:
// A radial gradient might not fill the layer...first, fill it with the end color
UIColor(red: bgColors[bgColors.count-4], green: bgColors[bgColors.count-3], blue: bgColors[bgColors.count-2], alpha: bgColors[bgColors.count-1]).setFill()
CGContextAddPath(context, bgPath); // Not sure why I need this...TODO: Investigate
CGContextFillPath(context)
let endRadius = hypot(gradientStartInPoints.x-gradientEndInPoints.x, gradientStartInPoints.y-gradientEndInPoints.y)
CGContextDrawRadialGradient(context, gradient, gradientStartInPoints, 0, gradientStartInPoints, endRadius, [])
}
case .Solid:
trueBackgroundColor.setFill()
CGContextFillPath(context)
}
CGContextRestoreGState(context); // Now we are no longer clipped to the path
// Border
CGContextAddPath(context, borderPath);
CGContextStrokePath(context);
}
// MARK: Convenience
func removeAllSubviews()
{
for view in subviews
{view.removeFromSuperview()}
}
}
You can create a CAGradientLayer, then add mask with only two corner radius to it. Then you can animate the transform using Core Animation, or UIView transform animation also should work if you put the layer in UIView.
CAGradientLayer * rectangleGradient = [CAGradientLayer layer];
rectangleGradient.colors = #[(id)[UIColor greenColor].CGColor, (id)[UIColor orangeColor].CGColor];
rectangleGradient.startPoint = CGPointMake(0.5, 0);
rectangleGradient.endPoint = CGPointMake(0.5, 1);
rectangleMask.path = maskPath;
////The animation
CABasicAnimation * theTransformAnim = [CABasicAnimation animationWithKeyPath:#"transform"];
theTransformAnim.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];;
theTransformAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(2, 2, 1)];;
theTransformAnim.duration = 1;
[rectangleGradient addAnimation:theTransformAnim];

NSWindow positioning

I'm the author of a Hearthstone tracker, and I have to move several NSWindow over Hearthstone window.
I get the frame of Hearthstone using CGWindowListCopyWindowInfo.
Then, I have to move my windows at some positions relative to Hearthstone.
The red arrows are over opponent cards, green arrow is over turn button and blue arrows are at the left and right of the window.
My actual screen setup is the following :
which gives me the following frames
// screen 1 : {x 0 y 0 w 1.440 h 900}
// screen 2 : {x 1.440 y -180 w 1.920 h 1.080}
To place the opponent tracker (the left frame) at the right position, which is the most simple case, I use {x 0 y somepadding w 185 h hearthstoneHeight - somepadding} and get the correct frame with this
func relativeFrame(frame: NSRect) -> NSRect {
var relative = frame
relative.origin.x = NSMinX(hearthstoneFrame) + NSMinX(frame)
relative.origin.y = NSMinY(hearthstoneFrame) + NSMinY(frame)
return relative
}
The right tracker is placed using {x hearthstoneWidth - trackerWidth, ...}
For other overlays, I used my current (Hearthstone) resolution to place them and them calculate them using a simple math
x = x / 1404.0 * NSWidth(hearthstoneFrame)
y = y / 840.0 * NSHeight(hearthstoneFrame)
This works pretty well. Except if I use my second screen. In this case, the frames seems to be correct, but the position of the window is not good.
Here is a screenshot of a debug window with {x 0 y 0 w hearthstoneWidth h hearthsoneHeight }. If I compare the frames of Hearthstone and my overlay, they are identical.
The complete function is the following (I'm in a "static class", I only show revelant code). I guess I'm missing something in the calculation but I can't find what.
class frameRelative {
static var hearthstoneFrame = NSZeroRect
static func findHearthstoneFrame() {
let options = CGWindowListOption(arrayLiteral: .ExcludeDesktopElements)
let windowListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0))
if let info = (windowListInfo as NSArray? as? [[String: AnyObject]])?
.filter({
!$0.filter({ $0.0 == "kCGWindowName" && $0.1 as? String == "Hearthstone" }).isEmpty
})
.first {
var rect = NSRect()
let bounds = info["kCGWindowBounds"] as! CFDictionary
CGRectMakeWithDictionaryRepresentation(bounds, &rect)
rect.size.height -= titleBarHeight // remove the 22px from the title
hearthstoneFrame = rect
}
}
static func frameRelative(frame: NSRect, _ isRelative: Bool = false) -> NSRect {
var relative = frame
var pointX = NSMinX(relative)
var pointY = NSMinY(relative)
if isRelative {
pointX = pointX / 1404.0 * NSWidth(hearthstoneFrame)
pointY = pointY / 840.0 * NSHeight(hearthstoneFrame)
}
let x: CGFloat = NSMinX(hearthstoneFrame) + pointX
let y = NSMinY(hearthstoneFrame) + pointY
relative.origin = NSMakePoint(x, y)
return relative
}
}
// somewhere here
let frame = NSMakeRect(0, 0, hearthstoneWidth, hearthstoneHeight)
let relativeFrame = SizeHelper.frameRelative(frame)
myWindow.setFrame(relativeFrame, display: true)
Any help will be appreciate :)
I eventually solved this issue so I decided to post the answer to close this thread... and maybe if someone face the same issue one day.
The solution was to substract the max y from the first screen with the max y of the Hearthstone window.
The final code of findHearthstoneFrame is
static func findHearthstoneFrame() {
let options = CGWindowListOption(arrayLiteral: .ExcludeDesktopElements)
let windowListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0))
if let info = (windowListInfo as NSArray? as? [[String: AnyObject]])?.filter({
!$0.filter({ $0.0 == "kCGWindowName"
&& $0.1 as? String == "Hearthstone" }).isEmpty
}).first {
if let id = info["kCGWindowNumber"] as? Int {
self.windowId = CGWindowID(id)
}
var rect = NSRect()
let bounds = info["kCGWindowBounds"] as! CFDictionary
CGRectMakeWithDictionaryRepresentation(bounds, &rect)
if let screen = NSScreen.screens()?.first {
rect.origin.y = NSMaxY(screen.frame) - NSMaxY(rect)
}
self._frame = rect
}
}
And the frameRelative is
static let BaseWidth: CGFloat = 1440.0
static let BaseHeight: CGFloat = 922.0
var scaleX: CGFloat {
return NSWidth(_frame) / SizeHelper.BaseWidth
}
var scaleY: CGFloat {
// 22 is the height of the title bar
return (NSHeight(_frame) - 22) / SizeHelper.BaseHeight
}
func frameRelative(frame: NSRect, relative: Bool = true) -> NSRect {
var pointX = NSMinX(frame)
var pointY = NSMinY(frame)
let width = NSWidth(frame)
let height = NSHeight(frame)
if relative {
pointX = pointX * scaleX
pointY = pointY * scaleY
}
let x: CGFloat = NSMinX(self.frame) + pointX
let y: CGFloat = NSMinY(self.frame) + pointY
let relativeFrame = NSRect(x: x, y: y, width: width, height: height)
return relativeFrame
}