I am using the below to create a gradient view that is visible in IB:
import UIKit
import QuartzCore
#IBDesignable
class FAUGradientView: UIView {
#IBInspectable var firstColor:UIColor = UIColor.clear
#IBInspectable var secondColor:UIColor = UIColor.clear
#IBInspectable var startPoint:CGPoint = CGPoint(x: 0.0, y: 1.0)
#IBInspectable var endPoint:CGPoint = CGPoint(x: 1.0, y:0.0)
var gradientLayer:CAGradientLayer!
override func draw(_ rect: CGRect) {
super.draw(rect)
gradientLayer = CAGradientLayer()
self.gradientLayer.colors = [firstColor, secondColor]
self.gradientLayer.startPoint = self.startPoint
self.gradientLayer.endPoint = self.endPoint
self.gradientLayer.frame = self.frame
self.layer.addSublayer(self.gradientLayer)
}
}
However, what I get in IB is a solid black view, not a view with a two color gradient, as seen below:
Solved it. They need to be cgColors, but XCode doesn't give you a single error to indicate that.
[firstColor.cgColor, secondColor.cgColor]
The above fixes the issue.
Works well, but;
self.gradientLayer.frame = self.frame
should be
self.gradientLayer.frame = self.bounds
or you can get some very odd draw offsets depending on where the view is within the parent
Related
My image is not visible in Scenekit, I also tested while button is visible, image button or button with image background becomes invisible. Any idea?
I have finally showed an image over Scenekit View, but not using image view. I have checked out Fox2SceneKitWWDC2017 sample project and I see they used SpriteKit overlay. I used Fox2 sample codes to do so, as below. In case some body needs...
//
// Overlay.swift
// Remote
//
// Created by Mustafa Akkuzu on 14.12.2021.
//
import Foundation
import SceneKit
import SpriteKit
class Overlay: SKScene {
private var overlayNode: SKNode
// MARK: - Initialization
init(size: CGSize, controller: ViewController) {
overlayNode = SKNode()
super.init(size: size)
scaleMode = .resizeFill
addChild(overlayNode)
// Assign the SpriteKit overlay to the SceneKit view.
isUserInteractionEnabled = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func showImage() {
// Congratulation title
let congratulationsNode = SKSpriteNode(imageNamed: "circle.png")
overlayNode.addChild(congratulationsNode)
let w: CGFloat = size.width
let h: CGFloat = size.height
overlayNode.position = CGPoint(x: w/2, y: h/2)
// Animate
congratulationsNode.alpha = 0.0
congratulationsNode.xScale = 0
congratulationsNode.yScale = 0
congratulationsNode.run( SKAction.group([SKAction.fadeIn(withDuration: 0.25),
SKAction.sequence([SKAction.scale(to: 1.22, duration: 0.25),
SKAction.scale(to: 1.0, duration: 0.1)])]))
}
}
Usage:
let overlay = Overlay(size: sceneView.bounds.size, controller: self)
sceneView.overlaySKScene = overlay
overlay.showImage()
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"))
I am trying to have a zoom animation run on a layer-backed NSView by animating the transform of the backing layer. The issue I am having with this, is that the animation zooms into the bottom left corner instead of the center of the view. I figured out that this is because NSView sets its backing layer's anchor point to (0, 0), even after I change it to some other value. This post talks about a similar issue.
I know that to get around this, I could make the view a layer-hosting view. However, I would like to use auto layout, which is why that is not really an option.
Does anyone know another way to get around this behavior and keep the anchor point of the view's backing layer at (0.5, 0.5)? The excerpt from apple's documentation in the post I linked above talks about NSView cover methods. What could such cover method be for the anchor point?
Thanks a lot!
The trick is to override the backing layer and pass an anchor point of choice (to be able to zoom from top left, for instance). Here's what I use:
extension CGPoint {
static let topLeftAnchor: Self = .init(x: 0.0, y: 1.0)
static let bottomLeftAnchor: Self = .init(x: 0.0, y: 0.0)
static let topRightAnchor: Self = .init(x: 1.0, y: 1.0)
static let bottomRightAnchor: Self = .init(x: 1.0, y: 0.0)
static let centerAnchor: Self = .init(x: 0.5, y: 0.5)
}
class AnchoredLayer: CALayer {
public var customAnchorPoint = CGPoint.topLeftAnchor
override var anchorPoint: CGPoint {
get { customAnchorPoint }
set { super.anchorPoint = customAnchorPoint }
}
}
class AnchoredView: NSView {
required convenience init(anchoredTo point: CGPoint) {
self.init(frame: .zero)
self.wantsLayer = true
self.anchorPoint = point
}
public override func makeBackingLayer() -> CALayer {
let roundedLayer = AnchoredLayer()
return roundedLayer
}
public var anchorPoint: CGPoint {
get { (layer as! AnchoredLayer).customAnchorPoint }
set { (layer as! AnchoredLayer).customAnchorPoint = newValue }
}
}
Then use AnchoredView as normal:
let myView = AnchoredView(anchoredTo: .topLeftAnchor)
// Create the scale animation
let transformScaleXyAnimation = CASpringAnimation()
transformScaleXyAnimation.fillMode = .forwards
transformScaleXyAnimation.keyPath = "transform.scale.xy"
transformScaleXyAnimation.toValue = 1
transformScaleXyAnimation.fromValue = 0.8
transformScaleXyAnimation.stiffness = 300
transformScaleXyAnimation.damping = 55
transformScaleXyAnimation.mass = 0.8
transformScaleXyAnimation.initialVelocity = 4
transformScaleXyAnimation.duration = transformScaleXyAnimation.settlingDuration
myView.layer?.add(transformScaleXyAnimation, forKey: "transformScaleXyAnimation")
...
Swift 4, macOS 10.13
I have read a variety of answers on SO and still can't get an NSImageView to spin at its center instead of one of its corners.
Right now, the image looks like this (video): http://d.pr/v/kwiuwS
Here is my code:
//`loader` is an NSImageView on my storyboard positioned with auto layout
loader.wantsLayer = true
let oldFrame = loader.layer?.frame
loader.layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
loader.layer?.position = CGPoint(x: 0.5, y: 0.5)
loader.layer?.frame = oldFrame!
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(-1 * .pi * 2.0)
rotateAnimation.duration = 2
rotateAnimation.repeatCount = .infinity
loader.layer?.add(rotateAnimation, forKey: nil)
Any ideas what I am still missing?
I just created a simple demo which contains the handy setAnchorPoint extension for all views.
The main reason you see your rotation from a corner is that your anchor point is somehow reset to 0,0.
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
var imageView: NSImageView!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
// Create red NSImageView
imageView = NSImageView(frame: NSRect(x: 100, y: 100, width: 100, height: 100))
imageView.wantsLayer = true
imageView.layer?.backgroundColor = NSColor.red.cgColor
window.contentView?.addSubview(imageView)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationDidBecomeActive(_ notification: Notification) {
// Before animate, reset the anchor point
imageView.setAnchorPoint(anchorPoint: CGPoint(x: 0.5, y: 0.5))
// Start animation
if imageView.layer?.animationKeys()?.count == 0 || imageView.layer?.animationKeys() == nil {
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = 0
rotate.toValue = CGFloat(-1 * .pi * 2.0)
rotate.duration = 2
rotate.repeatCount = Float.infinity
imageView.layer?.add(rotate, forKey: "rotation")
}
}
}
extension NSView {
func setAnchorPoint(anchorPoint:CGPoint) {
if let layer = self.layer {
var newPoint = NSPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
var oldPoint = NSPoint(x: self.bounds.size.width * layer.anchorPoint.x, y: self.bounds.size.height * layer.anchorPoint.y)
newPoint = newPoint.applying(layer.affineTransform())
oldPoint = oldPoint.applying(layer.affineTransform())
var position = layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
layer.anchorPoint = anchorPoint
layer.position = position
}
}
}
As I wondered many times myself on this question, here is my own simple method to rotate any NSView. I post it also as a self reminder. It can be defined in a category if needed.
This is a simple rotation, not a continuous animation. Should be applied to an NSView instance with wantsLayer = YES.
- (void)rotateByNumber:(NSNumber*)angle {
self.layer.position = CGPointMake(NSMidX(self.frame), NSMidY(self.frame));
self.layer.anchorPoint = CGPointMake(.5, .5);
self.layer.affineTransform = CGAffineTransformMakeRotation(angle.floatValue);
}
This is the result of a layout pass resetting your view's layer to default properties. If you check your layer's anchorPoint for example, you'll find it's probably reset to 0, 0.
A simple solution is to continually set the desired layer properties in viewDidLayout() if you're in a view controller. Basically doing the frame, anchorPoint, and position dance that you do in your initial setup on every layout pass. If you subclassed NSImageView you could likely contain that logic within that view, which would be much better than putting that logic in a containing view controller.
There is likely a better solution with overriding the backing layer or rolling your own NSView subclass that uses updateLayer but I'd have to experiment there to give a definitive answer.
Does anyone know what the code would be to make a background image that would cover the whole GameScene.swift
This is my code atm -
import SpriteKit
class GameScene: SKScene {
let playbutton = SKSpriteNode (imageNamed: "play")
let score = SKSpriteNode (imageNamed: "score")
var background = SKSpriteNode (imageNamed: "background")
override func didMoveToView(view: SKView) {
func loadBackGround()
{
background = SKSpriteNode(imageNamed: "background")
background.name = "background"
background.zPosition = 1.0
background.size = self.scene.size
scene.addChild(background)
}
self.playbutton.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
self.addChild(self.playbutton)
self.score.position = CGPointMake(CGRectGetMidX(self.frame), 100)
self.addChild(self.score)
You may have to be more specific (iOS Game, Mac Game...), but I think something like this should work:
var bgImage:SKSpriteNode
func loadBackGround()
{
bgImage = SKSpriteNode(imageNamed: "BACKGROUND IMAGE NAME")
bgImage.name = "bg"
bg.size = self.scene.size
scene.addChild(bg)
}
(you can use it outside of a function of course)
EDIT: Try this code:
import SpriteKit
class GameScene: SKScene {
let playbutton = SKSpriteNode (imageNamed: "play")
let score = SKSpriteNode (imageNamed: "score")
var background = SKSpriteNode ()
func loadBackGround() // You define the function and what it does
{
background = SKSpriteNode(imageNamed: "background")
background.name = "background"
background.size = self.scene.size
scene.addChild(background)
}
override func didMoveToView(view: SKView) {
loadBackGround() // You call the function you defined
self.playbutton.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
self.addChild(self.playbutton)
self.score.position = CGPointMake(CGRectGetMidX(self.frame), 100)
self.addChild(self.score)
}
This is the way I use:
override init(size: CGSize) {
super.init(size: size)
anchorPoint = CGPoint(x: 0.5, y: 0.5)
let background = SKSpriteNode(imageNamed: "Background")
addChild(background)
}
This loads the background image from the asset catalog and places it in the scene. Because the scene’s anchorPoint is (0.5, 0.5), the background image will always be centered on the screen on both 3.5-inch and 4-inch devices.
I get this from a good tutorial: How to Make a Game Like Candy Crush with Swift Tutorial: Part 1