Spritekit and swiftui, change scene a better way - swift

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")
}
}
}

Related

RealityKit removing old 3D objects from scene before adding new 3D objects

I'm trying to play with Apple's RealityKit and some usdz files I got from their website.
I made a small app where I can load each 3D model at a time.
It works but with one issue. If I add a single 3D object is fine but when I load the new models they all get stuck upon each other. I can't seem to figure out how to remove the old 3D object before adding a different one.
Here is my code
My Model object
import Foundation
import UIKit
import RealityKit
import Combine
class Model {
var modelName: String
var image: UIImage
var modelEntity: ModelEntity?
private var cancellable: AnyCancellable? = nil
var message: String = ""
init(modelName: String){
self.modelName = modelName
self.image = UIImage(named: modelName)!
let filename = modelName + ".usdz"
self.cancellable = ModelEntity.loadModelAsync(named: filename)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Model.swift -> DEBUG: Succesfully loaded \(self.modelName)")
break
case .failure(let error):
print("Model.swift -> DEBUG: Unable to load : \(self.modelName)")
self.message = error.localizedDescription
}
}, receiveValue: { modelEntity in
//get model entity
self.modelEntity = modelEntity
})
}
}
RealityKit and SwiftUI View
import SwiftUI
import RealityKit
import ARKit
struct ContentView : View {
#State private var isPlacementEnabled = false
#State private var selectedModel: Model?
#State private var modelConfirmedForPlacement: Model?
private var models: [Model] {
//Dynamicaly get file names
let filemanager = FileManager.default
guard let path = Bundle.main.resourcePath,
let files = try? filemanager.contentsOfDirectory(atPath: path)
else {
return []
}
var availableModels: [Model] = []
for filename in files where filename.hasSuffix("usdz") {
let modelName = filename.replacingOccurrences(of: ".usdz", with: "")
let model = Model(modelName: modelName)
availableModels.append(model)
}
return availableModels
}
var body: some View {
ZStack(alignment: .bottom){
ARViewContainer(modelConfirmedForPlacement: self.$modelConfirmedForPlacement)
if self.isPlacementEnabled {
PlacementButtonsView(isPlacementEnabled: self.$isPlacementEnabled,
selectedModel: self.$selectedModel,
modelConfirmedForPlacement: self.$modelConfirmedForPlacement
)
}
else {
ModelPickerView(models: self.models,
isPlacementEnabled: self.$isPlacementEnabled,
selectedModel: self.$selectedModel)
}
}
}
}
struct ARViewContainer: UIViewRepresentable {
#Binding var modelConfirmedForPlacement: Model?
var anchorEntity = AnchorEntity(world: .init(x: 0, y: 0, z: 0))
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let config = ARWorldTrackingConfiguration()
config.planeDetection = [.horizontal, .vertical ]
config.environmentTexturing = .automatic
if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh){
config.sceneReconstruction = .mesh
}
arView.session.run(config)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
//Here is where I try to remove the old 3D Model before loading the new one
if self.modelConfirmedForPlacement?.modelEntity != nil {
uiView.scene.anchors.first?.removeChild((self.modelConfirmedForPlacement?.modelEntity)!)
anchorEntity.scene?.removeAnchor(anchorEntity)
self.anchorEntity.removeChild((self.modelConfirmedForPlacement?.modelEntity)!)
}
if let model = self.modelConfirmedForPlacement{
if model.modelEntity != nil {
print("ARContainer -> DEBUG: adding model to scene - \(model.modelName)" )
}
else{
print("ARContainer -> DEBUG: Unable to load model entity for - \(model.modelName)" )
}
DispatchQueue.main.async {
let modelEntity = try? Entity.load( named: model.modelName + ".usdz")
anchorEntity.addChild(modelEntity!)
uiView.scene.addAnchor(anchorEntity)
}
}
}
}
struct ModelPickerView: View {
var models: [Model]
#Binding var isPlacementEnabled: Bool
#Binding var selectedModel: Model?
var body: some View {
ScrollView(.horizontal, showsIndicators: false){
HStack(spacing: 30){
ForEach(0 ..< self.models.count , id: \.self){ index in
Button {
// print("DEBUG : selected model with name: \(self.models[index].modelName)")
self.isPlacementEnabled = true
self.selectedModel = self.models[index]
} label: {
Image(uiImage: self.models[index].image)
.resizable()
.frame(height: 80 )
.aspectRatio(1/1, contentMode: .fit)
.background(Color.white)
.cornerRadius(12)
}
.buttonStyle(PlainButtonStyle())
}
}
}
.padding(20)
.background(Color.black.opacity(0.5))
}
}
struct PlacementButtonsView: View {
#Binding var isPlacementEnabled: Bool
#Binding var selectedModel: Model?
#Binding var modelConfirmedForPlacement: Model?
var body: some View {
HStack{
//Cancel button
Button {
// print("DEBUG: model ploacement cancelled")
self.resetPlacementParameters()
} label: {
Image(systemName: "xmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.75))
.cornerRadius(30)
.padding(20)
}
//Confirm button
Button {
// print("DEBUG: model ploacement confirmed")
self.modelConfirmedForPlacement = self.selectedModel
self.resetPlacementParameters()
} label: {
Image(systemName: "checkmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.75))
.cornerRadius(30)
.padding(20)
}
}
}
func resetPlacementParameters(){
self.isPlacementEnabled = false
self.selectedModel = nil
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif

SceneKit change `.allowCameraControl` without refreshing

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)

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?

How to change app tint color (New SwiftUI life cycle app)?

What is the best way to change app tint color in a SwiftUI app?
It is powered by the new SwiftUI lifecycle so I do not have the option to perform self.?tintColor
Tried searching here but didn't find any way to do it in a SwiftUI lifecycle app.
In the SceneDelegate.swift where you create the window for your app you can set the tint color globally using the tintColor property of UIWindow
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
self.window?.tintColor = UIColor.red // Or any other color you want
window.makeKeyAndVisible()
}
Edit
After seeing that you want it for the new SwiftUI, you can create new EnvironmentKeys:
private struct TintKey: EnvironmentKey {
static let defaultValue: Color = Color.blue
}
extension EnvironmentValues {
var tintColor: Color {
get { self[TintKey.self] }
set { self[TintKey.self] = newValue }
}
}
#main
struct YourApp: App {
var body: some Scene {
WindowGroup {
ContentView().environment(\.tintColor, Color.red)
}
}
}
Then in your views you would use it like this:
struct ContentView: View {
#Environment(\.tintColor) var tintColor
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Button(action: {}, label: {
Text("Button")
})
}.accentColor(tintColor)
}
}
This works without an EnvironmentKey, and propagates to all views in the app:
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.accentColor(.red) // pick your own color
}
}
}
Ariel,
In this implementation,
you can choose and use 'accentColor' throughout the entire application lifecycle. However, if you want to keep a value permanently,
you should think about a solution,
I hope you are a smart guy ...
enum MyColor: Identifiable, CaseIterable {
var id: String { UUID().uuidString }
case blue, green, orange, pink, purple, red, yellow
var currentColor: Color {
switch self {
case .blue:
return .blue
case .green:
return .green
case .orange:
return .orange
case .pink:
return .pink
case .purple:
return .purple
case .red:
return .red
case .yellow:
return .yellow
}
}
}
final class ViewModel: ObservableObject {
#Published
var isPresented = false
#Published
var myColor: MyColor = .blue
}
struct AppSettings: View {
#ObservedObject
var vm: ViewModel
var body: some View {
NavigationView {
Form {
Picker("Current color", selection: $vm.myColor) {
ForEach(MyColor.allCases) { color in
Label(
title: {
Text(color.currentColor.description.capitalized)
}, icon: {
Image(systemName: "circle.fill")
})
.tag(color)
.foregroundColor(color.currentColor)
}
}
}
.navigationBarItems(
trailing:
Button(
action: {
vm.isPresented.toggle()},
label: {
Text("Close")
}))
.navigationTitle("App settings")
}
}
}
struct ContentView: View {
#StateObject
private var vm = ViewModel()
var body: some View {
VStack {
Button(
action: {
vm.isPresented.toggle()
}, label: {
VStack {
Rectangle()
.fill(Color.accentColor)
.frame(width: 100, height: 100)
Text("Settings")
.font(.title)
}
})
}
.accentColor(vm.myColor.currentColor)
.sheet(
isPresented: $vm.isPresented) {
AppSettings(vm: vm)
.accentColor(vm.myColor.currentColor)
}
}
}

Passing an EnvronmentObject to NSHostingControllers

I'm using a custom SwiftUI View using NSSplitViewController that takes in ViewBuilders for the two subviews. My problem is any change in state of the environment doesn't propagate to the subviews inside SplitView, but propagates to another TextView in ContentView
import SwiftUI
class AppEnvironment : ObservableObject {
#Published var value: String = "default"
}
struct ContentView: View {
#EnvironmentObject var env : AppEnvironment
var body: some View {
HStack {
Button(action: {
self.env.value = "new value"
}, label: { Text("Change value") })
Text(self.env.value)
GeometryReader { geometry in
SplitView(master: {
Text("master")
.background(Color.yellow)
}, detail: {
HStack {
Text(self.env.value) }
.background(Color.orange)
}).frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//
// Source: https://gist.github.com/HashNuke/f8895192fff1f275e66c30340f304d80
//
struct SplitView<Master: View, Detail: View>: View {
var master: Master
var detail: Detail
init(#ViewBuilder master: () -> Master, #ViewBuilder detail: () -> Detail) {
self.master = master()
self.detail = detail()
}
var body: some View {
let viewControllers = [NSHostingController(rootView: master), NSHostingController(rootView: detail)]
return SplitViewController(viewControllers: viewControllers)
}
}
struct SplitViewController: NSViewControllerRepresentable {
var viewControllers: [NSViewController]
private let splitViewResorationIdentifier = "com.company.restorationId:mainSplitViewController"
func makeNSViewController(context: Context) -> NSViewController {
let controller = NSSplitViewController()
controller.splitView.dividerStyle = .thin
controller.splitView.autosaveName = NSSplitView.AutosaveName(splitViewResorationIdentifier)
controller.splitView.identifier = NSUserInterfaceItemIdentifier(rawValue: splitViewResorationIdentifier)
let vcLeft = viewControllers[0]
let vcRight = viewControllers[1]
vcLeft.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
vcRight.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 70).isActive = true
let sidebarItem = NSSplitViewItem(contentListWithViewController: vcLeft)
sidebarItem.canCollapse = false
// I'm not sure if this has any impact
// controller.view.frame = CGRect(origin: .zero, size: CGSize(width: 800, height: 800))
controller.addSplitViewItem(sidebarItem)
let mainItem = NSSplitViewItem(viewController: vcRight)
controller.addSplitViewItem(mainItem)
return controller
}
func updateNSViewController(_ nsViewController: NSViewController, context: Context) {
print("should update splitView")
}
}
Yes, in such case EnvironmentObject is not injected automatically. The solution would be to separate content into designated views (for better design) and inject environment object manually.
Here it is
Text(self.env.value)
GeometryReader { geometry in
SplitView(master: {
MasterView().environmentObject(self.env)
}, detail: {
HStack {
DetailView().environmentObject(self.env)
}).frame(width: geometry.size.width, height: geometry.size.height)
}
and views
struct MasterView: View {
var body: some View {
Text("master")
.background(Color.yellow)
}
}
struct DetailView: View {
var body: some View {
HStack {
Text(self.env.value) }
.background(Color.orange)
}
}