macOS Swift CALayer.contents does not load image - macos

The code below creates a red rectangle that is animated to move across the view from left to right. I would like to have an arbitrary shape loaded from an image to either superimpose or replace the rectangle. However, the circleLayer.contents = NSImage statement in the initializeCircleLayer function doesn't produce any effect. The diagnostic print statement seems to verify that the image exists and has been found, but no image appears in the view. How do I get an image into the layer to replace the animated red rectangle? Thanks!
CODE BELOW:
import Cocoa
class ViewController: NSViewController {
var circleLayer = CALayer()
override func viewDidLoad() {
super.viewDidLoad()
self.view.wantsLayer = true
initializeCircleLayer()
simpleCAAnimationDemo()
}
func initializeCircleLayer(){
circleLayer.bounds = CGRect(x: 0, y: 0, width: 150, height: 150)
circleLayer.position = CGPoint(x: 50, y: 150)
circleLayer.backgroundColor = NSColor.red.cgColor
circleLayer.cornerRadius = 10.0
let testIm = NSImage(named: NSImage.Name(rawValue: "testImage"))
print("testIm = \(String(describing: testIm))")
circleLayer.contents = NSImage(named: NSImage.Name(rawValue: "testImage"))?.cgImage
circleLayer.contentsGravity = kCAGravityCenter
self.view.layer?.addSublayer(circleLayer)
}
func simpleCAAnimationDemo(){
circleLayer.removeAllAnimations()
let animation = CABasicAnimation(keyPath: "position")
let startingPoint = NSValue(point: NSPoint(x: 50, y: 150))
let endingPoint = NSValue(point: NSPoint(x: 600, y: 150))
animation.fromValue = startingPoint
animation.toValue = endingPoint
animation.repeatCount = Float.greatestFiniteMagnitude
animation.duration = 10.0
circleLayer.add(animation, forKey: "linearMovement")
}
}

Why it doesn't work
The reason why
circleLayer.contents = NSImage(named: NSImage.Name(rawValue: "testImage"))?.cgImage
doesn't work is because it's a reference to the cgImage(forProposedRect:context:hints:) method, meaning that its type is
((UnsafeMutablePointer<NSRect>?, NSGraphicsContext?, [NSImageRep.HintKey : Any]?) -> CGImage?)?
You can see this by assigning NSImage(named: NSImage.Name(rawValue: "testImage"))?.cgImage to a local variable and ⌥-clicking it to see its type.
The compiler allows this assignment because circleLayer.contents is an Any? property, so literally anything can be assigned to it.
How to fix it
As of macOS 10.6, you can assign NSImage objects to a layers contents directly:
circleLayer.contents = NSImage(named: NSImage.Name(rawValue: "testImage"))

Related

Applying a gradient effect on an SKSpriteNode in Swift 5.0

iOS 14, Swift 5.x
Trying to add a gradient to a spriteNode using SKEffect. Code compiles, but then crashes, am I attempting the impossible here.
let image2U = UIImage(named: "2140983")?.ciImage
let effectsNode = SKEffectNode()
let filter = CIFilter(name: "CILinearGradient")
let startColor = UIColor.red
let endColor = UIColor.yellow
let startVector = CIVector(cgPoint: CGPoint(x: 0, y: 0))
let endVector = CIVector(cgPoint: CGPoint(x: box.size.width, y: box.size.height))
filter?.setDefaults()
filter?.setValue(startVector, forKey: "inputPoint0")
filter?.setValue(endVector, forKey: "inputPoint1")
filter?.setValue(startColor, forKey: "inputColor0")
filter?.setValue(endColor, forKey: "inputColor1")
filter?.setValue(image2U, forKey: "inputImage")
effectsNode.filter = filter
self.addChild(effectsNode)
effectsNode.addChild(box)
Compiles, but then crashes with this message ...
2021-07-09 21:08:47.584142+0200 GameIV[19791:1140737] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<CILinearGradient 0x600002070d20> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key InputImage.'
And as you can see I added an inputImage? Tried a UIImage, same error... tried no image at all, same error?
Probably you have a typo in the key name. Maybe "inputImage" instead of "InputImage". Usually you'd use one of the constants to avoid these. There's a partial list at https://developer.apple.com/documentation/coreimage/cifilter/filter_parameter_keys
The answer to my question is that SKEffectNodes need an input image, that is what it is trying to tell me here. And the gradient filter doesn't need/work with an image, it simply creates a new image. This is CIFilter code to do just that.
extension UIImage {
func returnCheckerboard() -> UIImage {
let context = CIContext(options: nil)
let checkerFilter = CIFilter.checkerboardGenerator()
checkerFilter.color0 = .white
checkerFilter.color1 = .black
checkerFilter.center = CGPoint(x: 0, y: 0)
checkerFilter.sharpness = 1
checkerFilter.width = 8
guard let outputImage = checkerFilter.outputImage else { return UIImage() }
if let cgimg = context.createCGImage(outputImage, from: CGRect(x: 0, y: 0, width: 128, height: 128)) {
let filteredImage = UIImage(cgImage: cgimg)
return filteredImage
}
return UIImage()
}
}
This doesn't create a gradient, but the principle is the same. It creates a checkerboard, doesn't require an input image and couldn't/isn't a CIFilter you can use with SKEffectNode.

Uitextfield background color blurry?

I am trying to make the background color of a UItextfield blurry. When I try the code below, my app crashes when it runs. Has anyone tried this before and knows how to make a UITextfield blurry?
let p = UITextField()
let blurEffect = UIBlurEffect(style: .light)
let blurView = UIVisualEffectView(effect: blurEffect)
p.layer.isOpaque = true
p.layer.backgroundColor = blurView as! CGColor
I found a solution where you place a view behind the UITextfield, and make it transparent.
let v = UIView()
v.frame = CGRect(x: 30, y: 100, width: 180, height: 30)
let blurEffect = UIBlurEffect(style: .light)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.frame = v.bounds
blurView.backgroundColor = .clear
v.addSubview(blurView)
let p = UITextField()
p.frame = CGRect(x: 0, y: 0, width: 180, height: 30)
p.layer.isOpaque = true
p.backgroundColor = .clear
v.addSubview(p)
self.view.backgroundColor = .red
self.view.addSubview(v)
This is an example of proposed solution, with background image instead of red color, to emphasize the blur effect
self.view.backgroundColor = UIColor(patternImage: UIImage(named: "background") ?? UIImage())

NSImageView (added programmatically) doesn't show image, but shows color

Swift 4. Very simple project, all I did - just added a NSImageView programmatically, backgroundColor and NSImage from the .jpg file. I see the good pink color, but can't see the image at all! I tried many different approaches and some was successful (Image showed up well in collection view and if NSImageView was added manually in the story board) but I need in simple programmatically method. Here is all of my code:
class ViewController: NSViewController {
var image: NSImage = NSImage()
var ivTest = NSImageView()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.ivTest)
self.ivTest.wantsLayer = true
self.ivTest.layer?.backgroundColor = NSColor.systemPink.cgColor
self.ivTest.layer?.frame = NSRect(x: 0, y: 0, width: 100, height: 100)
let manager = FileManager.default
var url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
url = url?.appendingPathComponent("night.jpg")
image = NSImage(byReferencing: url!)
if (image.isValid == true){
print("valid")
print("image size \(image.size.width):\(image.size.height)")
self.ivTest.image = image
} else {
print("not valid")
}
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
output:
result:
thank so much...
--- edited ---
Yes, thank You! Just added this and saw image:
self.ivTest.frame = NSRect(x: 0, y: 0, width: 100, height: 100)

Text Field with Only top and Bottom Border using swift 2

I have a page for forgot password. It has only a text field asking the user to fill in their email address. The Designer designed the text field with top and bottom border only.
I tried answer from here UITextField Only Top And Bottom Border
but in the result it only shows bottom border for the text field.
Like in the image i would like to create a grey border for top and bottom
To remove Fights with views you could create a tableView with a static cell that contains a TextField. Voila done... Top and bottom border comes for free and you will use standard apple stuff :)
If you really want to draw the layers than follow the steps on your linked questions:
CALayer *topBorder = [CALayer layer];
topBorder.frame = CGRectMake(0, 0, self.frame.size.width, 1);
topBorder.backgroundColor = [UIColor blackColor].CGColor;
[myTextField.layer addSublayer:topBorder];
CALayer *bottomBorder = [CALayer layer];
bottomBorder.frame = CGRectMake(0, self.frame.size.height - 1, self.frame.size.width, 1);
bottomBorder.backgroundColor = [UIColor blackColor].CGColor;
[myTextField.layer addSublayer:bottomBorder];
In Swift:
let topBorder = CALayer()
topBorder.frame = CGRectMake(0, 0, bounds.size.width, 1)
topBorder.backgroundColor = UIColor.grayColor()
textField.layer.addSublayer(topBorder)
let bottomBorder = CALayer()
bottomBorder.frame = CGRectMake(0, bounds.size.height-1, bounds.size.width, 1)
bottomBorder.backgroundColor = UIColor.grayColor()
textField.layer.addSublayer(bottomBorder)
Thanks #El Captain for the valuable comment and nice answer by #Bjorn Ro even if it was in Objective-c i think.
And my answer for the question is (i'm using swift 2 Xcode 7)
Override the function viewDidLayoutSubviews() in your swift file. And the Code for the same is
override func viewDidLayoutSubviews() {
// Creates the bottom border
let borderBottom = CALayer()
let borderWidth = CGFloat(2.0)
borderBottom.borderColor = UIColor.grayColor().CGColor
borderBottom.frame = CGRect(x: 0, y: forgotPasswordEmailText.frame.height - 1.0, width: forgotPasswordEmailText.frame.width , height: forgotPasswordEmailText.frame.height - 1.0)
borderBottom.borderWidth = borderWidth
forgotPasswordEmailText.layer.addSublayer(borderBottom)
forgotPasswordEmailText.layer.masksToBounds = true
// Creates the Top border
let borderTop = CALayer()
borderTop.borderColor = UIColor.grayColor().CGColor
borderTop.frame = CGRect(x: 0, y: 0, width: forgotPasswordEmailText.frame.width, height: 1)
borderTop.borderWidth = borderWidth
forgotPasswordEmailText.layer.addSublayer(borderTop)
forgotPasswordEmailText.layer.masksToBounds = true
}
forgotPasswordEmailText is the text field for entering Email
The Final output looks like this... with a gray Colour border (Screen shot of iPhone 4s Simulator)
Good suggestions for programatic solution posted so far. But I figured I'd share an Interfacebuilder solution....
1) Create view collection in your view controller
#IBOutlet private var borderViews: [UIView]?
2) Create 2 UIViews in interface builder 1px high constrained to where you want them around the textfield
3) Connect the 2 views in interface builder to borderViews IBOutlet
4) Customise appearance of both views by using setValue forKeyPath... for example, on success you may want the border to turn green
setValue(UIColor.green, forKeyPath: "borderViews.backgroundColor")
In Swift 3 use extension:
Create Swift file
import UIKit
extension UITextField {
func setBottomBorder() {
self.borderStyle = .none
self.layer.backgroundColor = UIColor.white.cgColor
self.layer.masksToBounds = false
self.layer.shadowColor = UIColor.gray.cgColor
self.layer.shadowOffset = CGSize(width: 0.0, height: 1.0)
self.layer.shadowOpacity = 1.0
self.layer.shadowRadius = 0.0
}
}
Call from anywhere:
PasswordField.setBottomBorder();
Here's a nice and easy Swift 4 implementation that handles resizing views as well :)
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
viewToShadow.backgroundColor = .white
viewToShadow.layer.masksToBounds = false
viewToShadow.layer.sublayers?
.filter { layer -> Bool in
return layer.backgroundColor == UIColor.almostBlack.alpha(0.5).cgColor
}
.forEach { layer in
layer.removeFromSuperlayer()
}
[CGRect(x: 0.0, y: 0.0, width: viewToShadow.bounds.width, height: 0.5),
CGRect(x: 0.0, y: viewToShadow.bounds.height, width: viewToShadow.bounds.width, height: 0.5)]
.forEach { frame in
let layer = CALayer()
layer.frame = frame
layer.backgroundColor = UIColor.almostBlack.alpha(0.5).cgColor
viewToShadow.layer.addSublayer(layer)
}
}
Use handy extension for it
extension UITextField {
func addTopBorder(){
let bottomLine = CALayer()
bottomLine.frame = CGRect.init(x: 0, y: 0, width: self.frame.size.width, height: 1)
bottomLine.backgroundColor = UIColor.white.cgColor
self.borderStyle = UITextField.BorderStyle.none
self.layer.addSublayer(bottomLine)
}
func addBottomBorder(){
let bottomLine = CALayer()
bottomLine.frame = CGRect.init(x: 0, y: self.frame.size.height - 1, width: self.frame.size.width, height: 1)
bottomLine.backgroundColor = UIColor.white.cgColor
self.attributedPlaceholder = NSAttributedString(string: self.placeholder ?? "-", attributes: [NSAttributedString.Key.foregroundColor : #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)])
self.borderStyle = UITextField.BorderStyle.none
self.layer.addSublayer(bottomLine)
}
}
use it in you controller like this
yourTextfield.addTopBorder()
yourTextfield.addBottomBorder()
and don't forget to use it on main thread
DispatchQueue.main.async {
self.yourTextfield.addTopBorder()
self.yourTextfield.addBottomBorder()
}

CustomView looks odd in my project, but good in the playground

So I've created a custom NSButton to have a beautiful radio button, but I'm experiencing a very weird bug.
My radio button looks good in the playground, but when I add it to my project, it looks odd.
Here are screenshots:
Left = in the playground.
Right = in my project.
As you can see, on the right (in my project), the blue dot looks horrible, it's not smooth, same thing for the white circle (it's less visible with the dark background).
In my project, the NSShadow on my CALayer is also flipped, even if the geometryFlipped property on my main (_containerLayer_) CALayer is set to true. -> FIXED: see #Bannings answer.
import AppKit
extension NSColor {
static func colorWithDecimal(deviceRed deviceRed: Int, deviceGreen: Int, deviceBlue: Int, alpha: Float) -> NSColor {
return NSColor(
deviceRed: CGFloat(Double(deviceRed)/255.0),
green: CGFloat(Double(deviceGreen)/255.0),
blue: CGFloat(Double(deviceBlue)/255.0),
alpha: CGFloat(alpha)
)
}
}
extension NSBezierPath {
var CGPath: CGPathRef {
return self.toCGPath()
}
/// Transforms the NSBezierPath into a CGPathRef
///
/// :returns: The transformed NSBezierPath
private func toCGPath() -> CGPathRef {
// Create path
let path = CGPathCreateMutable()
var points = UnsafeMutablePointer<NSPoint>.alloc(3)
let numElements = self.elementCount
if numElements > 0 {
var didClosePath = true
for index in 0..<numElements {
let pathType = self.elementAtIndex(index, associatedPoints: points)
switch pathType {
case .MoveToBezierPathElement:
CGPathMoveToPoint(path, nil, points[0].x, points[0].y)
case .LineToBezierPathElement:
CGPathAddLineToPoint(path, nil, points[0].x, points[0].y)
didClosePath = false
case .CurveToBezierPathElement:
CGPathAddCurveToPoint(path, nil, points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y)
didClosePath = false
case .ClosePathBezierPathElement:
CGPathCloseSubpath(path)
didClosePath = true
}
}
if !didClosePath { CGPathCloseSubpath(path) }
}
points.dealloc(3)
return path
}
}
class RadioButton: NSButton {
private var containerLayer: CALayer!
private var backgroundLayer: CALayer!
private var dotLayer: CALayer!
private var hoverLayer: CALayer!
required init?(coder: NSCoder) {
super.init(coder: coder)
self.setupLayers(radioButtonFrame: CGRectZero)
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
let radioButtonFrame = CGRect(
x: 0,
y: 0,
width: frameRect.height,
height: frameRect.height
)
self.setupLayers(radioButtonFrame: radioButtonFrame)
}
override func drawRect(dirtyRect: NSRect) {
}
private func setupLayers(radioButtonFrame radioButtonFrame: CGRect) {
//// Enable view layer
self.wantsLayer = true
self.setupBackgroundLayer(radioButtonFrame)
self.setupDotLayer(radioButtonFrame)
self.setupHoverLayer(radioButtonFrame)
self.setupContainerLayer(radioButtonFrame)
}
private func setupContainerLayer(frame: CGRect) {
self.containerLayer = CALayer()
self.containerLayer.frame = frame
self.containerLayer.geometryFlipped = true
//// Mask
let mask = CAShapeLayer()
mask.path = NSBezierPath(ovalInRect: frame).CGPath
mask.fillColor = NSColor.blackColor().CGColor
self.containerLayer.mask = mask
self.containerLayer.addSublayer(self.backgroundLayer)
self.containerLayer.addSublayer(self.dotLayer)
self.containerLayer.addSublayer(self.hoverLayer)
self.layer!.addSublayer(self.containerLayer)
}
private func setupBackgroundLayer(frame: CGRect) {
self.backgroundLayer = CALayer()
self.backgroundLayer.frame = frame
self.backgroundLayer.backgroundColor = NSColor.whiteColor().CGColor
}
private func setupDotLayer(frame: CGRect) {
let dotFrame = frame.rectByInsetting(dx: 6, dy: 6)
let maskFrame = CGRect(origin: CGPointZero, size: dotFrame.size)
self.dotLayer = CALayer()
self.dotLayer.frame = dotFrame
self.dotLayer.shadowColor = NSColor.colorWithDecimal(deviceRed: 46, deviceGreen: 146, deviceBlue: 255, alpha: 1.0).CGColor
self.dotLayer.shadowOffset = CGSize(width: 0, height: 2)
self.dotLayer.shadowOpacity = 0.4
self.dotLayer.shadowRadius = 2.0
//// Mask
let maskLayer = CAShapeLayer()
maskLayer.path = NSBezierPath(ovalInRect: maskFrame).CGPath
maskLayer.fillColor = NSColor.blackColor().CGColor
//// Gradient
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRect(origin: CGPointZero, size: dotFrame.size)
gradientLayer.colors = [
NSColor.colorWithDecimal(deviceRed: 29, deviceGreen: 114, deviceBlue: 253, alpha: 1.0).CGColor,
NSColor.colorWithDecimal(deviceRed: 59, deviceGreen: 154, deviceBlue: 255, alpha: 1.0).CGColor
]
gradientLayer.mask = maskLayer
//// Inner Stroke
let strokeLayer = CAShapeLayer()
strokeLayer.path = NSBezierPath(ovalInRect: maskFrame.rectByInsetting(dx: 0.5, dy: 0.5)).CGPath
strokeLayer.fillColor = NSColor.clearColor().CGColor
strokeLayer.strokeColor = NSColor.blackColor().colorWithAlphaComponent(0.12).CGColor
strokeLayer.lineWidth = 1.0
self.dotLayer.addSublayer(gradientLayer)
self.dotLayer.addSublayer(strokeLayer)
}
private func setupHoverLayer(frame: CGRect) {
self.hoverLayer = CALayer()
self.hoverLayer.frame = frame
//// Inner Shadow
let innerShadowLayer = CAShapeLayer()
let ovalPath = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -10, dy: -10))
let cutout = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -1, dy: -1)).bezierPathByReversingPath
ovalPath.appendBezierPath(cutout)
innerShadowLayer.path = ovalPath.CGPath
innerShadowLayer.shadowColor = NSColor.blackColor().CGColor
innerShadowLayer.shadowOpacity = 0.2
innerShadowLayer.shadowRadius = 2.0
innerShadowLayer.shadowOffset = CGSize(width: 0, height: 2)
self.hoverLayer.addSublayer(innerShadowLayer)
//// Inner Stroke
let strokeLayer = CAShapeLayer()
strokeLayer.path = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -0.5, dy: -0.5)).CGPath
strokeLayer.fillColor = NSColor.clearColor().CGColor
strokeLayer.strokeColor = NSColor.blackColor().colorWithAlphaComponent(0.22).CGColor
strokeLayer.lineWidth = 2.0
self.hoverLayer.addSublayer(strokeLayer)
}
}
let rbFrame = NSRect(
x: 87,
y: 37,
width: 26,
height: 26
)
let viewFrame = CGRect(
x: 0,
y: 0,
width: 200,
height: 100
)
let view = NSView(frame: viewFrame)
view.wantsLayer = true
view.layer!.backgroundColor = NSColor.colorWithDecimal(deviceRed: 40, deviceGreen: 40, deviceBlue: 40, alpha: 1.0).CGColor
let rb = RadioButton(frame: rbFrame)
view.addSubview(rb)
I'm using the exact same code on both my project and in the playground.
Here is a zip containing the playground and the project.
Just to be clear: I want to know why circles drawings are smooth in the playground but not in projects. (See #Bannings answer, it's more obvious with his screenshots)
Took time but I think I finally figured out everything or almost everything.
First some science : Circles or arcs can't be represented through Bézier curves. That's a property of Bézier curves as stated here : https://en.wikipedia.org/wiki/Bézier_curve
So when using NSBezierPath(ovalInRect:) you are in fact generating a Bézier curve approximating a circle. This can lead to a difference in appearance and how the shape renders. Still this shouldn't be a problem in our case because the difference is between two Bézier curves (the one in Playground and the one in real OS X project), but still I find it interesting to note in case you think the circle is not perfect enough.
Second as stated in this question (How to draw a smooth circle with CAShapeLayer and UIBezierPath) there are differences in the way the antialiasing will apply to your path depending on where the path is used. NSView's drawRect: being the place where the path antialiasing will be the best and CAShapeLayer being the worst.
Also I found that the CAShapeLayer documentation has a note saying this :
Shape rasterization may favor speed over accuracy. For example, pixels with multiple intersecting path segments may not give exact results.
Glen Low's answer to the question I previously mentioned seem to work fine in our case :
layer.rasterizationScale = 2.0 * self.window!.screen!.backingScaleFactor;
layer.shouldRasterize = true;
See the differences here :
Another solution is to leverage corner radiuses instead of Bézier paths in order to simulate a circle, and this time it's pretty accurate :
Finally my assumption on the difference between the Playground & the real OS X project is that Apple configured Playground so that some optimizations are turned off so the path even thought drawn using CAShapeLayer gets the best anti-aliasing possible. After all you're prototyping, performances are not really important especially on drawing operations.
I'm not sure to be right on this but I think it wouldn't be surprising. If anyone have any source I'd happily add it.
To me the best solution if you really need the best circle possible is to use corner radiuses.
Also as stated by #Bannings in another answer to this post. Shadows are reversed due to the fact playground render in a different coordinate system. See his answer to fix this.
I just fixed it by replace this line code:
self.dotLayer.shadowOffset = CGSize(width: 0, height: 2)
with:
self.dotLayer.shadowOffset = CGSize(width: 0, height: -2)
and replace innerShadowLayer.shadowOffset = CGSize(width: 0, height: 2) with:
innerShadowLayer.shadowOffset = CGSize(width: 0, height: -2)
I think you will get the same result like this:
Left = in the playground.
Right = in my project.
It seems the Playground is showing the bezier path in LLO coordinate system, you can visit the link:
https://forums.developer.apple.com/message/39277#39277

Resources