The SKView.showsFields = true only draws on a portion of the screen - nothing shows on the bottom-left.
This can be easily tested using Apple's default game playground. I only removed the action for the Hello World label and added a radialGravityField into the scene.
Also other fields like electricField do not draw anything on the screen.
Tested in Xcode 11.7 and Xcode 12.5.1. Is this a bug in the recent releases?
override func didMove(to view: SKView) {
// ...
let field = SKFieldNode.radialGravityField()
addChild(field)
}
and
sceneView.showsFields = true
Post
Replies
Boosts
Views
Activity
Is there a way to force precompilation of SKShader()s?
I am initializing a basic shader in my SKScene properties, but the shader only gets compiled when it is actually attached and rendered to a sprite.
class GameScene: SKScene {
let myShader = SKShader(fileNamed: "myEffect.fsh")
override func update(_ currentTime: TimeInterval) {
if touches.count > 0 {
mySprite.shader = myShader
// this is where the warning triggers for the first time
}
}
}
I know this because the scene pauses for a bit, immediately before the shader is rendered for the first time, and I also get the Metal warning (which seems to also be a known bug in recent releases - https://developer.apple.com/forums/thread/661774):
[Metal Compiler Warning] Warning: Compilation succeeded with:
program_source:3:19: warning: unused variable 's'
constexpr sampler s(coord::normalized,
I was expecting the shader to compile when it is initialized with SKShader(fileNamed:), since Apple docs says:
Compiling a shader and the uniform data associated with it can be expensive. Because of this, you should:
Initialize shader objects when your game launches, not while the game is running.
https://developer.apple.com/documentation/spritekit/skshader
SKFieldNode.isExclusive = true doesn't seem to work.
I created a very simple example in the Game Playground, with two intersecting electric fields and a third object placed at the intersection of the fields, with a negative charge.
Each field pulls the object towards its center. The first field (left side) has isExclusive=true, the second field (right side) has isExclusive=false.
So based on the definition, the exclusive field is suppressing any other field in its region of effect, and since the object is in both regions, it should be pulled towards the field that is exclusive.
Instead, the object doesn't move, since both fields are exerting the same forces on it.
The object gets pulled by both fields, thus not moving.
If one field is disabled using isEnabled = false, the object immediately starts to move towards the field that is enabled.
Playground code:
import PlaygroundSupport
import SpriteKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
/*
Configure two intersecting electric fields
*/
let field1 = SKFieldNode.electricField()
field1.region = SKRegion(radius: 190)
field1.position = CGPoint(x: -160, y: 0)
field1.isExclusive = true
addChild(field1)
let field2 = SKFieldNode.electricField()
field2.region = SKRegion(radius: 190)
field2.position = CGPoint(x: 160, y: 0)
field2.isExclusive = false
addChild(field2)
/*
Configure some visuals (shapes and labels) for the fields,
since SKView.showsFields doesn't work.
*/
let field1Visual = SKShapeNode(circleOfRadius: 190)
field1Visual.strokeColor = .yellow
field1Visual.addChild(SKLabelNode(text: "isExclusive=true"))
field1.addChild(field1Visual)
let field2Visual = SKShapeNode(circleOfRadius: 190)
field2Visual.addChild(SKLabelNode(text: "isExclusive=false"))
field2.addChild(field2Visual)
/*
Configure object placed at the intersection of the two electric fields,
with a negative charge, so each field attracts this object.
*/
let object = SKShapeNode(circleOfRadius: 25)
object.strokeColor = .magenta
object.physicsBody = SKPhysicsBody.init(circleOfRadius: 25)
object.physicsBody!.affectedByGravity = false
object.physicsBody!.charge = -0.1 // is attracted
addChild(object)
}
}
let sceneView = SKView(frame: CGRect(x:0 , y:0, width: 640, height: 480))
let scene = GameScene(size: CGSize(width: 1024, height: 768))
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
scene.scaleMode = .aspectFill
sceneView.presentScene(scene)
sceneView.showsFields = true // doesn't work for electricField() and is broken for other fields
PlaygroundSupport.PlaygroundPage.current.liveView = sceneView
A bug report was also submitted on Feedback assistant, FB9661601.
I am trying to use the isPaused: argument in the SpriteView initializer to pause the SKScene when a state property changes in SwiftUI.
@State private var showingLevelChooser = false
var body: some View {
SpriteView(scene: scene, isPaused: showingLevelChooser)
}
But when I use the state variable like:
SpriteView(scene: scene, isPaused: showingLevelChooser)
instead of just:
SpriteView(scene: scene)
the SKScene is recreated every time the variable changes, which is not what I want. I only want to pause the game.
As far as I understand this happens because SwiftUI recreates the views that are dependent on the state.
But if this is the case, how can you pause the SpriteKit scene from SwiftUI, without reinitializing it every time?
I created a sample Xcode 13 project here: https://github.com/clns/SpriteView-isPaused
The SKScene is displaying the time elapsed on the screen, in seconds. Every time the SwiftUI sheet is presented and the state variable changes, the timer starts from 0 (zero), because the scene is recreated.
A preview is included in the GitHub project. I cannot upload an animated gif here.
Use Case
If you have a ContentView that displays a pausable view based on a
@State private var presentSheet = false
property that is also used to present a
.sheet(isPresented: $presentSheet)
if:
the property is sent to the PausableView(isPaused: presentSheet) as a normal property, the body of the ContentView and the body of the sheet is being redrawn when the sheet is dismissed
the property is sent to the PausableView(isPaused: $presentSheet) as a @Binding, the body of the ContentView and the body of the sheet is NOT redrawn when the sheet is dismissed
Is this normal behavior?
The ContentView body makes sense to change, but is the sheet's body also supposed to be redrawn when the sheet is not presenting anymore after dismiss?
Sample Project
A sample project created in Xcode 13 is available here: https://github.com/clns/SwiftUI-sheet-redraw-on-dismiss. I noticed the same behavior on iOS 14 and iOS 15.
There are also 2 animated gifs showing the 2 different behaviors in the GitHub project.
The relevant code is in ContentView.swift:
import SwiftUI
struct DismissingView: View {
@Binding var isPresented: Bool
var body: some View {
if #available(iOS 15.0, *) {
print(Self._printChanges())
} else {
print("DismissingView: body draw")
}
return VStack {
Button("Dismiss") { isPresented.toggle() }
Text("Dismissing Sheet").padding()
}.background(Color.white)
}
}
struct PausableView: View {
var isPaused: Bool
// @Binding var isPaused: Bool
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State private var counter = 0
var body: some View {
Text("Elapsed seconds: \(counter)")
.onReceive(timer) { _ in
counter += isPaused ? 0 : 1
}
}
}
struct ContentView: View {
@State private var presentSheet = false
var body: some View {
if #available(iOS 15.0, *) {
print(Self._printChanges())
} else {
print("ContentView: body draw")
}
return VStack{
Button("Show Sheet") { presentSheet.toggle() }
Text("The ContentView's body along with the .sheet() is being redrawn immediately after dismiss, if the @State property `presentSheet` is used anywhere else in the view - e.g. passed to `PausableView(isPaused:presentSheet)`.\n\nBut if the property is passed as a @Binding to `PausableView(isPaused:$presentSheet)`, the ContentView's body is not redrawn.").padding()
PausableView(isPaused: presentSheet)
// PausableView(isPaused: $presentSheet)
}
.sheet(isPresented: $presentSheet) {
DismissingView(isPresented: $presentSheet)
.background(BackgroundClearView()) // to see what's happening under the sheet
}
}
}
Problem
It seems that SpriteView doesn't pause the SKScene with a state property passed to the SpriteView(scene:isPaused:) initializer.
Sample Project
I created a sample Xcode 13 project on GitHub running on an iOS 15 simulator with a ContentView with a @State var paused property that is sent to a child SpriteView(scene: scene, isPaused: paused). The state property is changed by a "Paused:" button. The property changes and the text on the button is updated, but the scene is never paused by the SpriteView.
It looks like the SpriteView is not picking up the updates and is not pausing the underlying SKView and SKScene inside it.
All the relevant code is in ContentView.swift:
import SwiftUI
import SpriteKit
class GameScene: SKScene, ObservableObject {
@Published var updates = 0
private let label = SKLabelNode(text: "Updates in SKScene:\n0")
override func didMove(to view: SKView) {
addChild(label)
label.numberOfLines = 2
}
override func update(_ currentTime: TimeInterval) {
updates += 1
label.text = "Updates in SKScene:\n\(updates)"
}
}
struct ContentView: View {
@State private var paused = false
@StateObject private var scene: GameScene = {
let scene = GameScene()
scene.size = CGSize(width: 300, height: 400)
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
scene.scaleMode = .fill
return scene
}()
var body: some View {
if #available(iOS 15.0, *) {
print(Self._printChanges())
}
return ZStack {
SpriteView(scene: scene, isPaused: paused).ignoresSafeArea()
VStack {
Text("Updates from SKScene: \(scene.updates)").padding().foregroundColor(.white)
Button("Paused: \(paused)" as String) {
paused.toggle()
}.padding()
Spacer()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Restart Problem
I also created a second GitHub project that is similar to the first one but it also has a "Restart" button that shows the same problem.
The "Restart" button should recreate the SKScene when pressed. The SKScene gets recreated inside the SceneStore and the ContentView's Text view (with a new time assigned to the name property of the scene), but the SpriteView doesn't change the scene.
It seems that the SpriteView keeps the initial scene in memory, and doesn't let it go to replace it with the new scene. This can be seen in the Console, by hitting the Restart button twice and looking for something like "-- Scene 7:49:44 PM deinit --".
The updates from the @Published var updates = 0 property inside the scene also stop (at the top of the screen), because the new scene that gets created is not added into the view, so the SKScene.didMove(to view:) method is never called.
The relevant code for this one is in ContentView.swift:
import SwiftUI
import SpriteKit
class GameScene: SKScene, ObservableObject {
@Published var updates = 0
private let label = SKLabelNode(text: "Updates in SKScene:\n0")
override func didMove(to view: SKView) {
addChild(label)
label.numberOfLines = 4
label.position = CGPoint(x: 0, y: -100)
}
override func update(_ currentTime: TimeInterval) {
updates += 1
label.text = "Updates in SKScene:\n\(updates)\nScene created at:\n\(name!)"
}
deinit {
print("-- Scene \(name!) deinit --")
}
}
class SceneStore : ObservableObject {
@Published var currentScene: GameScene
init() {
currentScene = SceneStore.createScene()
}
func restartLevel() {
currentScene = SceneStore.createScene()
}
// MARK: - Class Functions
static private func createScene() -> GameScene {
let scene = GameScene()
scene.size = CGSize(width: 300, height: 400)
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
scene.scaleMode = .fill
scene.name = Date().formatted(date: .omitted, time: .standard)
return scene
}
}
struct ContentView: View {
@EnvironmentObject private var sceneStore: SceneStore
@EnvironmentObject private var scene: GameScene
@State private var paused = false
var body: some View {
if #available(iOS 15.0, *) {
print(Self._printChanges())
}
return ZStack {
SpriteView(scene: scene, isPaused: paused).ignoresSafeArea()
VStack {
Text("Updates from SKScene: \(scene.updates)").padding().foregroundColor(.white)
Text("Scene created at: \(scene.name!)" as String).foregroundColor(.white)
Button("Restart") {
sceneStore.restartLevel()
}.padding()
Button("Paused: \(paused)" as String) {
paused.toggle()
}
Spacer()
}
}
}
}
Question / Workaround?
Am I missing something? Or is this a bug? If so, is there any workaround?
It seems that SpriteKit doesn't batch the draw calls for textures in the same texture atlas. There are two different behaviors, based on how the SKTextureAtlas gets initialized:
The draw calls are not batched when the texture atlas is initialized using SKTextureAtlas(named:) (loading the texture atlas from data stored in the app bundle).
But the draw calls seem to be batched when the texture atlas is created dynamically using SKTextureAtlas(dictionary:).
The following images show the two different behaviors and the SpriteAtlas inside the Assets Catalog.
1. Draw calls not batched:
2. Draw calls batched:
The SpriteAtlas:
I created a sample Xcode 13 project to show the different behavior: https://github.com/clns/spritekit-atlas-batching.
I have tried this on the iOS 15 simulator on macOS Big Sur 11.6.1.
The code to reproduce is very simple:
import SpriteKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
let atlas = SKTextureAtlas(named: "Sprites")
// let atlas = SKTextureAtlas(dictionary: ["costume": UIImage(named: "costume")!, "tank": UIImage(named: "tank")!])
let costume = SKSpriteNode(texture: atlas.textureNamed("costume"))
costume.setScale(0.3)
costume.position = CGPoint(x: 200, y: 650)
let tank = SKSpriteNode(texture: atlas.textureNamed("tank"))
tank.setScale(0.3)
tank.position = CGPoint(x: 500, y: 650)
addChild(costume)
addChild(tank)
}
}
Am I missing something?
While working with SVG textures, it seems that the Xcode Scene Editor is not displaying the texture in an SKReferenceNode. Instead, it just shows an X placeholder.
To replicate this, simply:
Create a new Game template project in Xcode
Add a new scene .sks file that contains a SKSpriteNode with an .svg texture
Drag the newly added .sks file into the main game scene and you'll see the X placeholder image instead of the proper texture
Has anyone dealt with this problem? Is there any known workaround to make Xcode display the textures properly?
This is using Xcode Version 13.1 (13A1030d) on macOS Big Sur 11.6.1.
Since Xcode 13.3 Beta can only be installed on macOS Monterey 12 or later, according to the Xcode 13.3 Beta Release Notes:
Xcode 13.3 beta includes SDKs for iOS 15.4, iPadOS 15.4, tvOS 15.4, watchOS 8.5, and macOS Monterey 12.3
Xcode 13.3 beta requires a Mac running macOS Monterey 12 or later.
Does this mean that older macs that can only run BigSur (e.g. Macbook Pro 2014) cannot develop apps for iOS 15.4+?
The scroll position is not updated when orientation changes in a ScrollView with .scrollTargetBehavior(.viewAligned) and .scrollPosition().
import SwiftUI
struct ContentView: View {
let colors: [Color] = [.red, .yellow, .cyan, .blue, .teal, .brown, .orange, .indigo]
@State private var selected: Int? = 0
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 0) {
ForEach(0..<colors.count, id: \.self) { index in
Rectangle()
.fill(colors[index])
.containerRelativeFrame(.horizontal)
.overlay {
Text(colors[index].description)
}
}
}
.scrollTargetLayout()
}
.scrollPosition(id: $selected)
.scrollTargetBehavior(.viewAligned)
}
}
#Preview {
ContentView()
}
@main
struct ViewAlignedScrollBugApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Tested on Xcode 15.3 (15E204a), iOS 17.3.1 iPhone, iOS 17.4 Simulator.
Bug report FB13685677 filed with Apple.
There seems to be a bug with SwiftUI's TabView when used with .tabViewStyle(.page) and any animated .transition() that moves the view horizontally (e.g. .move(edge: .leading/.trailing)), .slide, .offset(), etc.) in landscape orientation.
When the tab view transitions in, the content appears off-center and the animation goes back and forth before it stabilizes.
import SwiftUI
struct ContentView: View {
@State private var showTabView = false
var body: some View {
VStack {
Button("Toggle TabView") {
showTabView.toggle()
}
Spacer()
if showTabView {
TabView {
Text("Page One")
Text("Page Two")
}
.tabViewStyle(.page)
.transition(.slide)
}
}
.animation(.default, value: showTabView)
}
}
#Preview {
ContentView()
}
@main
struct TabViewTransitionBugApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Tested on Xcode 15.3 (15E204a), iOS 17.3.1 iPhone, iOS 17.4 Simulator.
Bug report FB13687638 filed with Apple.
The target view has .opacity() and .offset() modifiers in a scoped animation (iOS 17):
Text("Hello, world!")
.animation(.default) {
$0
.opacity(animate ? 1 : 0.2)
.offset(y: animate ? 0 : 100) // <-- DOESN'T WORK
}
But only the .opacity() works when the state is changed directly or withAnimation{}. The .offset() only works when using withAnimation{}, even though it should animate in both cases, like opacity.
Is this a SwiftUI bug? Did anyone encounter this?
import SwiftUI
struct ContentView: View {
@State private var animate = false
var body: some View {
VStack(spacing: 20) {
Button("Toggle Scoped Animation") {
animate.toggle()
}
Button("Toggle withAnimation{}") {
withAnimation {
animate.toggle()
}
}
Text("Hello, world!")
.animation(.default) {
$0
.opacity(animate ? 1 : 0.2)
.offset(y: animate ? 0 : 100) // <-- DOESN'T WORK
}
}
}
}
#Preview {
ContentView()
}
@main
struct ScopedAnimationOffsetBugApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Tested on Xcode 15.3 (15E204a), iOS 17.4 Simulator and iPhone Device.
Bug report FB13693703 filed with Apple.
After updating to Xcode 16.0 beta 5 (16A5221g) on macOS Sonoma 14.6 I am getting lots of errors like:
Main actor-isolated property '[property name]' can not be mutated from a nonisolated context
in classes that are marked as @MainActor at the class level, e.g.:
@MainActor final class MyClass: AnotherClass { ... }
The project uses Swift 6 and there are no errors in Xcode 16 beta 4.
I tried restarting, clearing Derived Data, cleaning build folder...
Anyone else encountering this issue with beta 5?