I'm trying to do this in Swift. However my SCNView doesn't show anything. I checked the connections in IB and everything is fine. I assume that I made an error while translating the source code from Objective C to Swift. Here is my code:
#IBOutlet var sceneview: SCNView
#IBOutlet var status: NSTextField
var statusCounter: Int = 1
#IBAction func paintRectButton (sender: AnyObject) {
status.stringValue = "Paint (#\(statusCounter++))"
var scene: SCNScene = SCNScene()
var cameraNode: SCNNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Make(0, 15, 30)
cameraNode.transform = CATransform3DRotate(cameraNode.transform, 7.0, 1, 0, 0)
scene.rootNode.addChildNode(cameraNode)
var spotlight: SCNLight = SCNLight()
spotlight.type = SCNLightTypeSpot
spotlight.color = NSColor.redColor()
var spotlightNode: SCNNode = SCNNode()
spotlightNode.light = spotlight
spotlightNode.position = SCNVector3Make(-2, 1, 0)
cameraNode.addChildNode(spotlightNode)
let boxSide = 15.0
var box: SCNBox =
SCNBox(width: boxSide, height: boxSide, length: boxSide, chamferRadius: 0)
var boxNode: SCNNode = SCNNode(geometry: box)
boxNode.transform = CATransform3DMakeRotation(3, 0, 1, 0)
scene.rootNode.addChildNode(boxNode)
sceneview.scene = scene
}
The reason why nothing shows up is that the camera is looking in a direction where there isn't any geometry object to be rendered. The Objective-C code uses -M_PI/7.0 (≈ -0.4488 radians) for the rotation angle of the camera and but your Swift code is using 7.0 (≈ 0.7168 radians (the remainder after dividing by π)). Change the Swift code to:
cameraNode.transform = CATransform3DRotate(cameraNode.transform, -M_PI/7.0, 1, 0, 0)
A similar mistake seem to have happened with the rotation of the box where the original code uses the angle M_PI_2/3 and the Swift code is using the angle 3.
boxNode.transform = CATransform3DMakeRotation(M_PI_2/3.0, 0, 1, 0)
Related
Goal: Add SceneKit Scene to SwiftUI MacOS project (not Catalyst) using UIViewRepresentable
What I did:
Following code works fine when target is SwiftUI iOS.
But when target is MacOS, I get error bellow:
import SwiftUI
import SceneKit
struct ScenekitView : UIViewRepresentable {
let scene = SCNScene(named: "art.scnassets/ship.scn")!
func makeUIView(context: Context) -> SCNView {
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = UIColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// retrieve the ship node
let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
// animate the 3d object
ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
// retrieve the SCNView
let scnView = SCNView()
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = true
// configure the view
scnView.backgroundColor = UIColor.black
}
}
#if DEBUG
struct ScenekitView_Previews : PreviewProvider {
static var previews: some View {
ScenekitView()
}
}
#endif
Really appreciate if anyone here can help!
Cheers to all!
Not much to change - just instead of UIViewRepresentable conform to NSViewRepresentable, and similar replacements from AppKit instead of UIKit
Here it is (tested with Xcode 11.2):
import SwiftUI
import SceneKit
import AppKit
struct ScenekitView : NSViewRepresentable {
let scene = SCNScene(named: "art.scnassets/ship.scn")!
func makeNSView(context: NSViewRepresentableContext<ScenekitView>) -> SCNView {
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = NSColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// retrieve the ship node
let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
// animate the 3d object
ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
// retrieve the SCNView
let scnView = SCNView()
return scnView
}
func updateNSView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = true
// configure the view
scnView.backgroundColor = NSColor.black
}
}
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"))
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.
I've just been playing around with some of the features of XCode/Swift and building a simple game. I'm having difficulty adding a physicsBody to an SKSpriteNode.
I've created a 'Bullet' class and a 'Tank' class (which has an array of 10 'Bullets' as one of its properties).
The sprite property is being assigned properly with a texture. However, the physicsBody is coming back as 'nil'.
class Tank {
var position = CGPoint()
var speed = CGPoint(x: 0, y: 0)
var sprite = SKSpriteNode()
var viewSize = CGPoint() // width, height
var bullets: [Bullet] = []
var bulletNumber = 0
let TOTAL_BULLETS = 10
// ***Tank class constructor
init(playerNum: Int, filename: NSString, tankName: NSString) {
var spaceshipTexture = SKTexture(imageNamed: "Spaceship")
self.sprite = SKSpriteNode(texture: spaceshipTexture, size: spaceshipTexture.size())
self.sprite.physicsBody? = SKPhysicsBody(texture: spaceshipTexture, size: spaceshipTexture.size())
println(self.sprite.physicsBody)
}
// ****Bullet class
class Bullet {
var position = CGPoint(x: -50, y: -50)
var speed = CGPoint(x: 0, y: 0)
var sprite = SKSpriteNode()
var viewSize = CGPoint() // width, height
var isBeingFired = false
init () {
//self.sprite = SKSpriteNode(imageNamed: "Bullet")
var spaceshipTexture = SKTexture(imageNamed: "Spaceship")
self.sprite = SKSpriteNode(texture: spaceshipTexture, size: spaceshipTexture.size())
self.sprite.xScale = 0.04
self.sprite.yScale = 0.04
self.sprite.physicsBody? = SKPhysicsBody(texture: spaceshipTexture, size: spaceshipTexture.size())
self.sprite.physicsBody?.affectedByGravity = false
println(self.sprite.physicsBody)
}
// *** GameScene
class GameScene: SKScene {
var tank1 = Tank(playerNum: 1, filename: "Spaceship", tankName: "Tank1")
let myLabel = SKLabelNode(fontNamed:"Chalkduster")
override func didMoveToView(view: SKView) {
self.addChild(tank1.sprite)
for i in 0 ..< tank1.TOTAL_BULLETS {
self.addChild(tank1.bullets[i].sprite)
}
For simplicity sake, I'm using the same image for both the Tank and the Bullet (just different sizes) to create the SKSpriteNode.
I'm not sure if this would help, but I add an instance of the Tank and all 10 of its bullets as a child to the GameScene.
Your problem is, that you don't add a SKTexture to your SKSpriteNode but an image. If you want to access an SKTexture you need to add it to your node like that:
var spaceshipTexture = SKTexture(imageNamed: "Spaceship")
var sprite = SKSpriteNode(texture: spaceshipTexture, size: spaceshipTexture.size())
Then you will be able to access the texture of your sprite to create your SKPhysicsBody
I have what should be a fairly simple question here. Basically I'm trying to move an object (a UIImageView) from a point A (where it is set in the storyboard) to a point B which I define programatically.
My first pass through this resulted in this code
UIView.animateWithDuration(0.75, delay: 0.5, options: UIViewAnimationOptions.CurveLinear, animations: {
self.signInButton.alpha = 1
self.signInButton.center.y = 10
}, completion: nil)
However, what this code does is to basically move the button offcenter and then back to its original location.
I then looked into QuartzCore to help me, but everything is in Objective-C. I have this method:
func moveImage(view: UIImageView){
var toPoint: CGPoint = CGPointMake(0.0, -10.0)
var fromPoint : CGPoint = CGPointZero
var movement = CABasicAnimation(keyPath: "movement")
movement.additive = true
movement.fromValue = fromPoint
movement.toValue = toPoint
movement.duration = 0.3
view.layer.addAnimation(movement, forKey: "move")
}
However the problem here is that movement.fromValue cannot accept a CGPoint. I know that in objective C there was a function that converted a CGPoint to a NSValue, however this function seems to be deprecated out of Swift and I can't find the other way of doing this.
Therefore my question is either, how do I convert CGPoint to NSValue to make my moveImage() function work, or is there a better way to move an object from point A to point B?
Thanks!
I'm looking at this question Animate UIImage in UIImageView Up & Down (Like it's hovering) Loop
Use NSValue(CGPoint: cgpiont) instead of NSValue.valueWithCGPoint(<#point: CGPoint#>) which is deprecated in swift. NSValue(CGPoint: cgpiont) is constructor given for that which can be used to convert CGPoint to NSValue
in swift.Following code will work
func moveImage(view: UIImageView){
var toPoint: CGPoint = CGPointMake(0.0, -10.0)
var fromPoint : CGPoint = CGPointZero
var movement = CABasicAnimation(keyPath: "movement")
movement.additive = true
movement.fromValue = NSValue(CGPoint: fromPoint)
movement.toValue = NSValue(CGPoint: toPoint)
movement.duration = 0.3
view.layer.addAnimation(movement, forKey: "move")
}
SWIFT 3 UPDATES
func moveImageView(imgView: UIImageView){
var toPoint:CGPoint = CGPoint(x: 0.0, y: -10.0)
var fromPoint:CGPoint = CGPoint.zero
var movement = CABasicAnimation(keyPath: "movement")
movement.isAdditive = true
movement.fromValue = NSValue(cgPoint: fromPoint)
movement.toValue = NSValue(cgPoint: toPoint)
movement.duration = 0.3
imgView.layer.add(movement, forKey: "move")
}