SceneKit change `.allowCameraControl` without refreshing - swift

I'm making a test program that allows the user to switch .allowCameraControl on and off with a button
So I'm using observable objects that update the scene object whenever a variable changes. But every time the .allowCameraControl option is changed, the scene refreshes and objects goes back to its original orientation.
How do I make it so that the object stays in its current orientation even when the .allowCameraControl changes
Here's a minimum reproducible example
ContentView.swift:
import SwiftUI
import SceneKit
final class Lmao: ObservableObject {
#Published var yo: SceneView.Options = [.allowsCameraControl]
}
struct ContentView: View {
#EnvironmentObject var l: Lmao
var scene = SCNScene(named: "myScene.scn")
var body: some View {
VStack {
SceneView(scene: scene, options: l.yo)
.frame(width: UIScreen.main.bounds.width ,
height: UIScreen.main.bounds.height / 2)
Button("allow/disable camera controll") {
if l.yo == [] {
l.yo = [SceneView.Options.allowsCameraControl]
}
else {
l.yo = []
}
}
}
}
func updateLocation(_ location: CGPoint) {
print(location)
}
}
CameraTestApp.swift:
#main
struct CameraTestApp: App {
#StateObject private var ll = Lmao()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(ll)
}
}
}
myScene.scn: put some random stuff in a scene file

Common approach
In ContentView use #State property wrapper allowOrNot for toggling states:
import SwiftUI
import SceneKit
struct ContentView : View {
#State private var text: String = "CamControl is On"
#State private var allowOrNot: Bool = true
var body: some View {
ZStack {
VRViewContainer(allowOrNot: $allowOrNot)
.ignoresSafeArea()
VStack {
Text(text)
.onTapGesture {
allowOrNot.toggle()
if !allowOrNot { text = "CamControl is OFF" }
else { text = "CamControl is On" }
}
Spacer()
}
}
}
}
In VRViewContainer use #Binding property wrapper.
struct VRViewContainer: UIViewRepresentable {
#Binding var allowOrNot: Bool
func makeUIView(context: Context) -> SCNView {
let sceneView = SCNView(frame: .zero)
let scene = SCNScene(named: "art.scnassets/ship.scn")
sceneView.scene = scene
return sceneView
}
func updateUIView(_ uiView: SCNView, context: Context) {
uiView.allowsCameraControl = allowOrNot
}
}
Your approach
If you wanna control a camera's transform using your code, you have to retrieve all the sixteen values ​​from the transform matrix (position, orientation and scale) of the scene's default camera node (or from transform of any other SCNCamera node). In both cases, you'll need the sceneView instance.
sceneView.pointOfView?.transform // SCNMatrix4
In simplified SwiftUI's SceneView init you've got the parameter called pointOfView:
SceneView(scene: SCNScene?, pointOfView: SCNNode?, options: SceneView.Options)

Related

How to make a text appear on the screen after pressing the button?

I want to make the text display on the screen according to the different scenes after pressing the button. For example, if model A is displayed, text "A" will be appeared on the screen. Similarly, if model B is displayed, text "B" will also be appeared. I am currently creating Augmented Reality app using SwiftUI interface and RealityKit but not sure what to do in the next step.
Here is my code:
import SwiftUI
import RealityKit
struct ContentView : View {
#State var arView = ARView(frame: .zero)
var body: some View {
ZStack {
ARViewContainer(arView: $arView)
HStack {
Spacer()
Button("information") {
print(self.arView.scene.name)
print(arView.scene.anchors.startIndex)
print(arView.scene.anchors.endIndex)
}
Spacer()
Button("remove") {
stop()
}
Spacer()
}
} .edgesIgnoringSafeArea(.all)
}
func stop() {
arView.scene.anchors.removeAll()
}
}
struct ARViewContainer: UIViewRepresentable {
#Binding var arView: ARView
func makeUIView(context: Context) -> ARView {
let boxAnchor = try! Experience1.loadBox()
let crownAnchor = try! Experience1.loadCrown()
arView.scene.anchors.append(boxAnchor)
arView.scene.anchors.append(crownAnchor)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) { }
}
From the code above, if boxAnchor and crownAnchor and displayed, text "Box" and "Crown" will be appeared on the screen respectively. Anyone who knows how to do that please guide me or suggest a tutorial that I can use to study.
Sorry if I use the wrong technical terms. Thank you
Use Combine's reactive subscriber and MVVM's bindings to update string values for Text views.
import SwiftUI
import RealityKit
import Combine
struct ContentView : View {
#State private var arView = ARView(frame: .zero)
#State private var str01: String = "...some text..."
#State private var str02: String = "...some text..."
var body: some View {
ZStack {
ARViewContainer(arView: $arView, str01: $str01, str02: $str02)
.ignoresSafeArea()
VStack {
Spacer()
Text(str01)
.foregroundColor(.white)
.font(.largeTitle)
Divider()
Text(str02)
.foregroundColor(.white)
.font(.largeTitle)
Spacer()
}
}
}
}
The miracle happens in the escaping closure of subscribe(to:) instance method. What will be the conditions in the if-statements is up to you.
struct ARViewContainer: UIViewRepresentable {
#Binding var arView: ARView
#Binding var str01: String
#Binding var str02: String
#State var subs: [AnyCancellable] = []
func makeUIView(context: Context) -> ARView {
let boxAnchor = try! Experience.loadBox()
let crownAnchor = try! Experience.loadCrown()
print(arView.scene.anchors.count)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
arView.scene.anchors.append(boxAnchor)
arView.scene.anchors.append(crownAnchor)
print(arView.scene.anchors.count)
}
return arView
}
func updateUIView(_ view: ARView, context: Context) {
DispatchQueue.main.async {
_ = view.scene.subscribe(to: SceneEvents.DidAddEntity.self) { _ in
if view.scene.anchors.count > 0 {
if view.scene.anchors[0].isAnchored {
str01 = "Crown"
str02 = "Cube"
}
}
}.store(in: &subs)
}
}
}

Spritekit and swiftui, change scene a better way

I wrote this simple few line of code to swap between 2 scene in Swiftui using SpriteKit, I'm try to understand if there could be a (better) different way to change from one scene to another using a button.
struct ContentView: View {
#StateObject var firstscene = FirstScene()
#StateObject var secondscene = SecondScene()
#State var changeScene = false
var body: some View {
ZStack{
if changeScene {
SpriteView(scene: firstscene)
} else {
SpriteView(scene: secondscene)
}
Button {
changeScene.toggle()
} label: {
Text("Change")
}
}
}
}
// the 2 SKScene created using a sub class of SKscene
class FirstScene: SKScene, ObservableObject {
let firstScene = SKScene(fileNamed: "FirstScene")
override func didMove(to view: SKView) {
scene?.view?.presentScene(firstScene)
}
}
class SecondScene: SKScene, ObservableObject {
let secondScene = SKScene(fileNamed: "SecondScene")
override func didMove(to view: SKView) {
scene?.view?.presentScene(secondScene)
}
}
now my doubt is, I'm change the SpriteView in the contentView using a var changeScene, could be done the same things in some other way using another approach?
open to any suggestions, I'm try to understand this framework .
thanks
You are using a standard way of switching views. You could clean your code up a bit like this:
struct ContentView: View {
#State var changeScene = false
var body: some View {
ZStack{
SpriteView(scene: (changeScene ? FirstScene() : SecondScene())) // this is a terniary operator
Button {
changeScene.toggle()
} label: {
Text("Change")
}
}
}
}
The new views do not need to be #StateObjects. You can call the View Types directly in your view.
Note: I am away from my computer so this is not tested.
iOS 14, Swift 5
This is tested and comes from this article.
https://betterprogramming.pub/build-a-game-of-chess-with-spritekit-3229c23bdba0
import SwiftUI
import SpriteKit
struct ContentView: View {
#State var switcher = false
var scene: SKScene {
let scene = GameScene.shared
scene.size = CGSize(width: 256, height: 512)
scene.scaleMode = .fill
scene.backgroundColor = .red
scene.name = "red"
return scene
}
var scene2: SKScene {
let scene2 = GameScene2.shared
scene2.size = CGSize(width: 256, height: 512)
scene2.scaleMode = .fill
scene2.backgroundColor = .blue
scene2.name = "blue"
return scene2
}
var body: some View {
if switcher {
SpriteView(scene: scene)
.frame(width: 256, height: 512)
.ignoresSafeArea()
.background(Color.red)
.onAppear {
scene2.isPaused = true
}
.onDisappear {
scene2.isPaused = false
}
} else {
SpriteView(scene: scene2)
.frame(width: 256, height: 512)
.ignoresSafeArea()
.background(Color.blue)
.onAppear {
scene.isPaused = true
}
.onDisappear {
scene.isPaused = false
}
}
Button {
withAnimation(.easeInOut(duration: 1.0)) {
switcher.toggle()
}
} label: {
Text("play")
}
}
}

pass / share class with SpriteView GameScene from a SwiftUi View

I am playing around with SpriteView, which works great. However I cannot figure out how to share a class with Spritekit GameScene,that is instantiated in one my views. Sharing my class with other views works like a charm. But how can I access my gameCenterManager class from spritekit's GameScene, how do I pass the class ? I don't have any clue how to do this. I considered making a global accessible class, but its not what I want.
The goal is to be able to send and receive data from inside GameScene to update players location and etc. But the matchmaking happens inside a SwiftUI View, which is also where the class is created.
The gameCenterManger class is created like so;
struct ContentView: View {
#StateObject var gameCenterManager = GameCenterManager()
var body: some View {
...
etc
the class is set up like so :
class GameCenterManager: NSObject, ObservableObject {
//lots of stuff going on
...
}
And is shared to the GameSceneView view that will create the SpriteView, like so :
Button("Show Sheet") {
self.showingSheet.toggle()
}
.buttonStyle(RoundedRectangleButtonStyle())
.buttonStyle(ShadowButtonStyle())
.fullScreenCover(isPresented: $showingSheet, content: { GameSceneView(gameCenterManager:self.gameCenterManager)})
Finally, inside my GameSceneView, the GameScene for SpriteView is configured.
struct GameSceneView: View {
var gameCenterManager: GameCenterManager
#Environment(\.presentationMode) var presentationMode
var scene: SKScene {
let scene = GameScene()
scene.size = CGSize(width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height)
scene.scaleMode = .fill
return scene
}
var body: some View {
Button("Dismiss") {
self.presentationMode.wrappedValue.dismiss()
}
Button("send data") {
gameCenterManager.increment()
}
SpriteView(scene: scene )
.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height - 200 )
.ignoresSafeArea()
.overlay(ImageOverlay(), alignment: .bottomTrailing)
}
}
Perhaps others are looking to do the same? Here below is the answer in code snippets
GameCenterManager is the class that I want to access across all views and SKScenes
In mainview:
#main
struct gameTestApp: App {
#StateObject var gameCenterManager = GameCenterManager()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(gameCenterManager)
}
}
}
In Contentview view add :
#EnvironmentObject var gameCenterManager:GameCenterManager
And where I transition to GameSceneView inside Contentview, which will load my SKScene
Button("Show Sheet") {
self.showingSheet.toggle()
}
.buttonStyle(RoundedRectangleButtonStyle())
.buttonStyle(ShadowButtonStyle())
.fullScreenCover(isPresented: $showingSheet, content: { GameSceneView()})
Then in GameSceneView add:
#EnvironmentObject var gameCenterManager:GameCenterManager
and where we load SKScene:
var scene: SKScene {
let scene = GameScene()
scene.size = CGSize(width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height)
scene.scaleMode = .fill
scene.gameCenterManager = gameCenterManager
return scene
}
Then finally in GameScene itself add:
var gameCenterManager: GameCenterManager?

SwiftUI playing Lottie animation

I was playing with the Lottie animations, these days, but I have some trouble how to stop them from the View.
Here is my ContentView, where I am playing the animation. I have one button which shows the boolean value in real-time:
struct ContentView: View {
#State var isShowing : Bool = true
var body: some View {
ZStack {
Color.green.opacity(0.3)
VStack {
VStack {
LottieDosi(isShowing: .constant(isShowing)){Text("")}
Button(action: {self.isShowing.toggle()}) {
Text("Button is \(String(isShowing))")
.padding(10)
.background(Color.white)
.cornerRadius(40)
}
}.padding(.horizontal, 30)
}
}.edgesIgnoringSafeArea(.all)
}
}
and here is my LottieView:
struct LottieView: UIViewRepresentable {
#Binding var isAnimating: Bool
#State var name : String
var loopMode: LottieLoopMode = .loop
var animationView = AnimationView()
func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
let view = UIView()
animationView.animation = Animation.named(name)
animationView.contentMode = .scaleAspectFill
animationView.loopMode = .loop
animationView.play()
animationView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animationView)
NSLayoutConstraint.activate([
animationView.widthAnchor.constraint(equalTo: view.widthAnchor),
animationView.heightAnchor.constraint(equalTo: view.heightAnchor)
])
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieView>) {
isAnimating ? animationView.play() : animationView.stop()
}
}
struct LottieDosi<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
VStack {
LottieView(isAnimating: .constant(isShowing), name: "13728-sticker-4")
.frame(width: 175, height: 175)
}
}
}
I have a binding bool which has to change itself either to true or false, whenever I tap on the button. In other words, when I press the button I should be able to play and stop it. But somehow, this boolean value is not shared between the structs and the animation is constantly playing. I am new to swift UI, so I assume I am doing something very wrong, therefore I will appreciate some help.
Here you can see my animation
You've almost got it. You need to construct a Coordinator so that you can access the values inside your LottieView. It's straight forward to do.
Create a Coordinator class that conforms to NSObject
Pass in your LottieView.
Add the function makeCoordinator to your LottieView
Create an instance of your Coordinator in the makeCoordinator function
In your updateUIView access the animationView from the coordinator.
You shouldn't need a binding for the isAnimating as the LottieView is not passing any information back to your ContentView. You also don't need that #State for the name
Here is how I have gotten Lottie to play and pause in my apps. Note that some of the variable names are different to yours but it should be enough to get you to where you need to be.
import SwiftUI
import Lottie
struct LottieView: UIViewRepresentable {
typealias UIViewType = UIView
let filename: String
let animationView = AnimationView()
let isPaused: Bool
func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
let view = UIView(frame: .zero)
let animation = Animation.named(filename)
animationView.animation = animation
animationView.contentMode = .scaleAspectFit
animationView.loopMode = .loop
animationView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animationView)
NSLayoutConstraint.activate([
animationView.widthAnchor.constraint(equalTo: view.widthAnchor),
animationView.heightAnchor.constraint(equalTo: view.heightAnchor),
])
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieView>) {
if isPaused {
context.coordinator.parent.animationView.pause()
} else {
context.coordinator.parent.animationView.play()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
var parent: LottieView
init(_ parent: LottieView) {
self.parent = parent
}
}
}
My ContentView looks like the following:
struct ContentView: View {
#State private var isPaused: Bool = true
var body: some View {
VStack {
LottieView(filename: "loading", isPaused: isPaused)
.frame(width: 200, height: 200)
Button(action: {
self.isPaused.toggle()
}, label: {
Text(isPaused ? "Play" : "Pause")
})
}
}
}
Here it is working:

SwiftUI with SceneKit: How to use button action from view to manipulate underlying scene

I’m trying to implement a very basic SwiftUI app with an underlying SceneKit scene/view. The buttons in the SwiftUI view should manipulate the content of the scene and vice versa the content of the scene should determine if the buttons are active or inactive.
Yes, I’ve read and watched the Apple developer sessions 226: Data Flow through SwiftUI and 231: Integrating SwiftUI. And I worked my way through the Apple tutorials. But I’m little lost here. Any hints and directions are appreciated.
Here is the code:
I have a MainView, which uses a SceneKit SCNView with a HUDView on top:
import SwiftUI
struct MainView: View {
var body: some View {
ZStack {
SceneView()
HUDView()
}
}
}
The SceneView integrates a SCNView via the UIViewRepresentable protocol. The scene has two functions to add and remove the box from the scene:
import SwiftUI
import SceneKit
struct SceneView: UIViewRepresentable {
let scene = SCNScene()
func makeUIView(context: Context) -> SCNView {
// create a box
scene.rootNode.addChildNode(createBox())
// code for creating the camera and setting up lights is omitted for simplicity
// …
// retrieve the SCNView
let scnView = SCNView()
scnView.scene = scene
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// configure the view
scnView.backgroundColor = UIColor.gray
// show statistics such as fps and timing information
scnView.showsStatistics = true
scnView.debugOptions = .showWireframe
}
func createBox() -> SCNNode {
let boxGeometry = SCNBox(width: 20, height: 24, length: 40, chamferRadius: 0)
let box = SCNNode(geometry: boxGeometry)
box.name = "box"
return box
}
func removeBox() {
// check if box exists and remove it from the scene
guard let box = scene.rootNode.childNode(withName: "box", recursively: true) else { return }
box.removeFromParentNode()
}
func addBox() {
// check if box is already present, no need to add one
if scene.rootNode.childNode(withName: "box", recursively: true) != nil {
return
}
scene.rootNode.addChildNode(createBox())
}
}
The HUDView bundles the buttons with actions to add and remove the box from the underlying scene. If the box object is in the scene, the add button should be inactive, only the remove button should be active:
struct HUDView: View {
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .center, spacing: 2) {
Spacer()
ButtonView(action: {}, icon: "plus.square.fill", isActive: false)
ButtonView(action: {}, icon: "minus.square.fill")
ButtonView(action: {}) // just a dummy button
}
.background(Color.white.opacity(0.2))
Spacer()
}
}
}
The buttons are fairly simple as well, they take an action and optionally an icon as well as their initial active/inactive state:
struct ButtonView: View {
let action: () -> Void
var icon: String = "square"
#State var isActive: Bool = true
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.title)
.accentColor(isActive ? Color.white : Color.white.opacity(0.5))
}
.frame(width: 44, height: 44)
}
}
The resulting app is pretty simple:
The SceneKit scene is rendered correctly and I am able to manipulate the camera on screen.
But how do I connect the button actions to the corresponding scene functions? How do I let the scene inform the HUDView about it’s content (box is present or not), so it can set the active/inactive state of the buttons?
What is the best approach to this task? Should I implement a Coordinator in SceneView? Should I use a separate SceneViewController implemented via the UIViewControllerRepresentable protocol?
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 #Bindingto set its active state (not sure about the correct order for this with the#State property inHUDView`):
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?
I answered a very similar question here. The idea is that you create a PassthroughSubject that your parent view (the one that owns the buttons) will send events down and your UIViewRespresentable will subscribe to those events.
There are many ways to do this stuff and you've obviously solved it - congrats!
You might check out this post 58103566 on game design layout. I'm sure there will be experts that disagree which is fine, and I don't want to cause a debate, just thought it might save you a few steps in the future.
From multiple VC's, I manipulate game objects and buttons often, usually without muddying up the water too much.
Hope it helps.