Swift Add Button to SCNNode
There are a few ways you can approach this:
1: Standard Approach:
In your delegate
method, assign each node a name as per below (clearly if you have lots of nodes you would want to store them in an Array or Dictionary depending upon your needs):
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. Check We Have The Image Anchor
guard let imageAnchor = anchor as? ARImageAnchor else { return }
//2. Get The Reference Image
let referenceImage = imageAnchor.referenceImage
//1. Create The Plane Geometry With Our Width & Height Parameters
let planeGeometry = SCNPlane(width: referenceImage.physicalSize.width,
height: referenceImage.physicalSize.height)
//2. Create A New Material
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
//3. Create The Plane Node
let planeNode = SCNNode(geometry: planeGeometry)
planeNode.geometry?.firstMaterial = material
planeNode.opacity = 0.25
planeNode.eulerAngles.x = -.pi / 2
//4. Add A Name To The Node
planeNode.name = "I Was Clicked"
//5. Add It To The Scene
node.addChildNode(planeNode)
}
Then using touchesBegan
, perform a hitTest and handle your touch accordingly:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
/*
1. Get The Current Touch Location
2. Check That We Have Touched A Valid Node
3. Check If The Node Has A Name
4. Handle The Touch
*/
guard let touchLocation = touches.first?.location(in: augmentedRealityView),
let hitNode = augmentedRealityView?.hitTest(touchLocation, options: nil).first?.node,
let nodeName = hitNode.name
else {
//No Node Has Been Tapped
return
}
//Handle Event Here e.g. PerformSegue
print(nodeName)
}
2: An Interesting Approach:
UIKit
elements can actually be added as an SCNGeometry's Material
. I personally haven't seen many people use this approach, but it may prove useful to anyone who wants to incorporate UIKit
with ARKit
.
Create A Custom UIButton e.g:
/// Clickable View
class ClickableView: UIButton{
override init(frame: CGRect) {
super.init(frame: frame)
self.addTarget(self, action: #selector(objectTapped(_:)), for: .touchUpInside)
self.backgroundColor = .red
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Detects Which Object Was Tapped
///
/// - Parameter sender: UIButton
@objc func objectTapped(_ sender: UIButton){
print("Object With Tag \(tag)")
}
}
And in your delegate method do the following:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. Check We Have The Image Anchor
guard let imageAnchor = anchor as? ARImageAnchor else { return }
//2. Get The Reference Image
let referenceImage = imageAnchor.referenceImage
//1. Create The Plane Geometry With Our Width & Height Parameters
let planeGeometry = SCNPlane(width: referenceImage.physicalSize.width,
height: referenceImage.physicalSize.height)
//2. Create A New Material
let material = SCNMaterial()
DispatchQueue.main.async {
//3. Create The New Clickable View
let clickableElement = ClickableView(frame: CGRect(x: 0, y: 0,
width: 300,
height: 300))
clickableElement.tag = 1
//4. Add The Clickable View As A Materil
material.diffuse.contents = clickableElement
}
//5. Create The Plane Node
let planeNode = SCNNode(geometry: planeGeometry)
planeNode.geometry?.firstMaterial = material
planeNode.opacity = 0.25
planeNode.eulerAngles.x = -.pi / 2
//6. Add It To The Scene
node.addChildNode(planeNode)
}
This should get you started...
added button to sceneKit view but it has a lag
Okay I solved it. The problematic piece of code is
starButton.addTarget(self, action: "starButtonClicked", forControlEvents: UIControlEvents.TouchUpInside)
UIControlEvent.TouchUpInside gives the illusion of lag. Changing it to .TouchDown is much better.
SwiftUI with SceneKit: How to use button action from view to manipulate underlying scene
I found a solution using @EnvironmentalObject
but I am not completely sure, if this is the right approach. So comments on this are appreciated.
First, I moved the SCNScene into it’s own class and made it an OberservableObject
:
class Scene: ObservableObject {
@Published var scene: SCNScene
init(_ scene: SCNScene = SCNScene()) {
self.scene = scene
self.scene = setup(scene: self.scene)
}
// code omitted which deals with setting the scene up and adding/removing the box
// method used to determine if the box node is present in the scene -> used later on
func boxIsPresent() -> Bool {
return scene.rootNode.childNode(withName: "box", recursively: true) != nil
}
}
I inject this Scene
into the app as an .environmentalObject()
, so it is available to all views:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Create the SwiftUI view that provides the window contents.
let sceneKit = Scene()
let mainView = MainView().environmentObject(sceneKit)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: mainView)
self.window = window
window.makeKeyAndVisible()
}
}
}
MainView
is slightly altered to call SceneView
(a UIViewRepresentable
) with the separate Scene
for the environment:
struct MainView: View {
@EnvironmentObject var scene: Scene
var body: some View {
ZStack {
SceneView(scene: self.scene.scene)
HUDView()
}
}
}
Then I make the Scene
available to the HUDView
as an @EnvironmentalObject
, so I can reference the scene and its methods and call them from the Button action. Another effect is, I can query the Scene
helper method to determine, if a Button should be active or not:
struct HUDView: View {
@EnvironmentObject var scene: Scene
@State private var canAddBox: Bool = false
@State private var canRemoveBox: Bool = true
var body: some View {
VStack {
HStack(alignment: .center, spacing: 0) {
Spacer ()
ButtonView(
action: {
self.scene.addBox()
if self.scene.boxIsPresent() {
self.canAddBox = false
self.canRemoveBox = true
}
},
icon: "plus.square.fill",
isActive: $canAddBox
)
ButtonView(
action: {
self.scene.removeBox()
if !self.scene.boxIsPresent() {
self.canRemoveBox = false
self.canAddBox = true
}
},
icon: "minus.square.fill",
isActive: $canRemoveBox
)
}
.background(Color.white.opacity(0.2))
Spacer()
}
}
}
Here is the ButtonView code, which used a @Binding
to set its active state (not sure about the correct order for this with the@State property in
HUDView`):
struct ButtonView: View {
let action: () -> Void
var icon: String = "square"
@Binding var isActive: Bool
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.title)
.accentColor(self.isActive ? Color.white : Color.white.opacity(0.5))
}
.frame(width: 44, height: 44)
.disabled(self.isActive ? false: true)
}
}
Anyway, the code works now. Any thoughts on this?
create 3 buttons or interactions ARKit swift
I'm not certain this is the most robust nor most scalable way to tackle your problem but I managed to achieve it fairly simply.
First I got an image which I knew was 10cm x 10cm:
I then added it as an ARReferenceImage
and set it's physicalWidth
and height
to (you guessed it) 0.1m x 0.1m.
Now since we know in advance the size of our detectable image, I created a simple SCNScene
.
I made an SCNPlane
with the same dimensions as the image, and set it's diffuse contents to our targetImage.
I then added three SCNTorus's
at the places I wanted to detect. Obviously this is me just being silly, but you will get the idea.
I then set each each of the pseudo buttons with a name:
(a) Spike (Top),
(b) Hand,
(c) Tail.
I then set the SCNPlane
to hidden
, as we dont actually need to see it.
After having setting up my ARImageTrackingConfiguration I used the delegate callback to overlay my scene:
//---------------------------
// MARK: - ARSCNViewDelegate
//---------------------------
extension ViewController: ARSCNViewDelegate{
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. Check We Have Detected An ARImageAnchor & Check It's The One We Want
guard let validImageAnchor = anchor as? ARImageAnchor,
let targetName = validImageAnchor.referenceImage.name, targetName == "TargetImage" else { return}
//2. Since We Know The Size Of Our Target & Have Created Our Overlay Simply Add It To The Node
guard let interactiveTarget = SCNScene(named: "art.scnassets/Overlay.scn") else { return }
let overlayNode = interactiveTarget.rootNode
overlayNode.eulerAngles.x = -.pi / 2
node.addChildNode(overlayNode)
}
}
That aligned very well, with the target image so, now I had to handle the interaction.
I simply used touchesBegan
to detect if we had hit any of our desired SCNNode
s:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//1. Get The Current Touch Location
//2. Check We Have Hit An SCNNode By Performing An SCNHitTest
//3. Check Our HitNode Has A Name & It Isn't The Target
guard let currentTouchLocation = touches.first?.location(in: self.augmentedRealityView),
let hitTestNode = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first?.node,
let name = hitTestNode.name, name != "Target" else { return }
//4. Update The UI That We Have Touched An Area
infoLabel.text = "Ya! Don't Touch My \(name)"
//5. Reset The Text
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.infoLabel.text = "" }
}
Which yielded something like this:
And here is the full code:
//---------------------------
// MARK: - ARSCNViewDelegate
//---------------------------
extension ViewController: ARSCNViewDelegate{
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. Check We Have Detected An ARImageAnchor & Check It's The One We Want
guard let validImageAnchor = anchor as? ARImageAnchor,
let targetName = validImageAnchor.referenceImage.name, targetName == "TargetImage" else { return}
//2. Since We Know The Size Of Our Target & Have Created Our Overlay Simply Add It To The Node
guard let interactiveTarget = SCNScene(named: "art.scnassets/Overlay.scn") else { return }
let overlayNode = interactiveTarget.rootNode
overlayNode.eulerAngles.x = -.pi / 2
node.addChildNode(overlayNode)
}
}
class ViewController: UIViewController {
@IBOutlet var augmentedRealityView: ARSCNView!
@IBOutlet weak var infoLabel: UILabel!
//-----------------------
// MARK: - View LifeCycle
//-----------------------
override func viewDidLoad() {
super.viewDidLoad()
augmentedRealityView.delegate = self
infoLabel.text = ""
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//1. Get The Images We Wish To Track
guard let imagesToTrack = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else {
fatalError("Missing Reference Images")
}
//2. Set Up Our ARTracking Configuration
let configuration = ARImageTrackingConfiguration()
configuration.maximumNumberOfTrackedImages = 1
//3. Assign Our Detection Images
configuration.trackingImages = imagesToTrack
//4. Run The Session
augmentedRealityView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
augmentedRealityView.session.pause()
}
//--------------------------
// MARK: - Touch Interaction
//--------------------------
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//1. Get The Current Touch Location
//2. Check We Have Hit An SCNNode By Performing An SCNHitTest
//3. Check Our HitNode Has A Name & It Isn't The Target
guard let currentTouchLocation = touches.first?.location(in: self.augmentedRealityView),
let hitTestNode = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first?.node,
let name = hitTestNode.name, name != "Target" else { return }
//4. Update The UI That We Have Touched An Area
infoLabel.text = "Ya! Don't Touch My \(name)"
//5. Reset The Text
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.infoLabel.text = "" }
}
}
As I said perhaps not the most robust or scalable solution but it will definitely point you in the right direction...
How to use *.scnp file in SwiftUI for button click (iOS)?
Here is modified code with demo. Tested with Xcode 11.4 / macOS 10.15.4
Note: as I don't know your project structure, all dependent resource files were added as Resources (not in Assets). Just in case.
struct DemoSceneKitParticles: View {
@State private var exploding = false
var body: some View {
VStack {
ScenekitView(exploding: $exploding)
Button("BOOM") { self.exploding = true }
}
}
}
struct ScenekitView : NSViewRepresentable {
@Binding var exploding: Bool
let scene = SCNScene(named: "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
if exploding {
if let ship = scene.rootNode.childNode(withName: "ship", recursively: true),
let particles = SCNParticleSystem(named: "Explosion", inDirectory: nil) {
let node = SCNNode()
node.addParticleSystem(particles)
node.position = ship.position
scnView.scene?.rootNode.addChildNode(node)
ship.removeFromParentNode()
}
}
}
}
Update: variant for iOS
Tested with Xcode 11.4 / iOS 13.4
Full module code (resource files as before at top level)
import SwiftUI
import SceneKit
struct DemoSceneKitParticles: View {
@State private var exploding = false
var body: some View {
VStack {
ScenekitView(exploding: $exploding)
Button("BOOM") { self.exploding = true }
}
}
}
struct ScenekitView : UIViewRepresentable {
@Binding var exploding: Bool
let scene = SCNScene(named: "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 exploding {
if let ship = scene.rootNode.childNode(withName: "ship", recursively: true),
let particles = SCNParticleSystem(named: "Explosion", inDirectory: nil) {
let node = SCNNode()
node.addParticleSystem(particles)
node.position = ship.position
scnView.scene?.rootNode.addChildNode(node)
ship.removeFromParentNode()
}
}
}
}
struct DemeSKParticles_Previews: PreviewProvider {
static var previews: some View {
DemoSceneKitParticles()
}
}
Is it possible to change the ARKit scene's object when clicking on a button Swift
What I was missing in my above code is that I had to make the deletion and add two synchronize tasks. Since the Delete function is into a closure( asynchronous task). so the add function will be executed before the deletion.
And by then the error will be gone.
Related Topics
How to Star a Repo with Github API
Crop Image According to Rectangle in Swiftui
Countforfetchrequest in Swift 2.0
Swift 3/Macos: Open Window on Certain Screen
Use Different Googleservice-Info.Plist for Single Project in Xcode Using Swift4
Swift: Copy Information Selected by User in Abpersonviewcontroller to Dictionary
How to Check If Airpods Are Connected to Iphone
Shared Cookies with Wkprocesspool for Wkwebview in Swift
How to Get a Double Value Up to 2 Decimal Places
Swift Enumeration Order and Comparison
Xcode Takes Long Time to Print Debug Results
Swiftui Sheet Not Animating Dismissal on MACos Big Sur
Swiftlint Overriding Project Settings Related to Spm
How to Draw Two Polylines in Different Colors in Mapkit