Troubleshoot `hasBeenConnected` SwiftUI
TODO: collapasable, copyable, download, etc
I want to trigger a view to call ``openImmersiveSpace when connectionStatusViewModel.hasBeenConnected
changes... I have ```import SwiftUI
import RealityKit
import RealityKitContent
struct ContentView: View {
@EnvironmentObject var connectionStatusViewModel: ControllerConnectionStatusViewModel
@Environment(\.openImmersiveSpace) var openImmersiveSpace
// TODO: call when we are dismissed ? exit app?
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace
var body: some View {
VStack {
ControllerConnectionStatusView(connectionStatusViewModel: connectionStatusViewModel)
GeometryReader { proxy in
let textWidth = min(max(proxy.size.width * 0.4, 300), 500)
ZStack {
HStack(spacing: 60) {
VStack(alignment: .leading, spacing: 0) {
Text("Laser Sword Trainer \(connectionStatusViewModel.hasBeenConnected)")
.font(.system(size: 50, weight: .bold))
.padding(.bottom, 15)
.onChange(of: connectionStatusViewModel.hasBeenConnected) {
oldValue, newValue in
print("onChange happened \(oldValue) \(newValue)")
if oldValue == false && newValue {
Task {
await openImmersiveSpace(id: "TrainerSpace")
}
}```
I don't see hasBeenConnected ever fire... what am I doing wrong? Spatial computing goes even further with visionOS updates that bring enhanced support for volumetric apps, powerful new enterprise APIs, the new TabletopKit framework, and more.
visionOS Highlights
TabletopKit Create collaborative experiences centered around a table. This new framework handles the manipulation of cards and pieces, establishing placement and layout, and defining game boards.
New enterprise APIs provide access to spatial barcode scanning, the Apple Neural Engine, object tracking parameters, and more.
visionOS for enterprise
Volumetric APIs • Create apps that take full advantage of depth and space — and can
run side by side with other apps in the Shared Space.
• Resize volumes by using the SwiftUI scene
modifier windowResizability.
Decide if you want the user’s hands to appear in front of or behind your digital content.
Inputs
Getting started? Explore the visionOS Pathway on developer.apple.com
• Detect planes in all orientations.
• Allow anchoring objects on surfaces in your surroundings.
• Use Room Anchors to consider the user’s surroundings on a
per-room basis.
• Attach content to individual objects around the user with the new
Object Tracking API for visionOS.
Updates to scene understanding
import pandas as pd
Load the documentation.csv and code.csv files
documentation_df = pd.read_csv('/mnt/data/documentation.csv') code_df = pd.read_csv('/mnt/data/code.csv')
Display the structure of the dataframes to understand their content
documentation_df.head(), code_df.head()( framework category title
0 arkit Framework ARKit
1 arkit Class ARKitSession
2 arkit Protocol DataProvider
3 arkit Protocol Anchor
4 arkit API Collection ARKit in visionOS
abstract \
0 Integrate hardware sensing features to produce...
1 The main entry point for receiving data from A...
2 A source of live data from ARKit.
3 The identity, location, and orientation of an ...
4 Create immersive augmented reality experiences.
content \
0 Framework ARKit Integrate hardware sensing fea...
1 ARKit ARKitSession Class ARKit Session The mai...
2 ARKit DataProvider Protocol Data Provider A so...
3 ARKit Anchor Protocol Anchor The identity, loc...
4 Collection ARKit ARKit in visionOS API Collect...
code_samples \
0 NaN
1 let\n planeData \n=\n \nPlaneDetectionProvider...
2 NaN
3 NaN
4 NaN
url
0 https://developer.apple.com/documentation/arkit
1 https://developer.apple.com/documentation/arki...
2 https://developer.apple.com/documentation/arki...
3 https://developer.apple.com/documentation/arki...
4 https://developer.apple.com/documentation/arki... ,
title
0 Work with windows in SwiftUI
1 Work with windows in SwiftUI
2 Work with windows in SwiftUI
3 Work with windows in SwiftUI
4 Work with windows in SwiftUI
description \
0 Learn how to create great single and multi-win...
1 Learn how to create great single and multi-win...
2 Learn how to create great single and multi-win...
3 Learn how to create great single and multi-win...
4 Learn how to create great single and multi-win...
code_title \
0 BOT-anist scenes
1 Creating the movie WindowGroup
2 Opening a movie window
3 Pushing a movie window
4 Toolbar
code_sample \
0 @main\n\n\nstruct\n \nBOTanistApp\n: \nApp\n {...
1 @main\n\n\nstruct\n \nBOTanistApp\n: \nApp\n {...
2 struct\n \nEditorContentView\n: \nView\n {\n ...
3 struct\n \nEditorContentView\n: \nView\n {\n ...
4 CanvasView\n()\n .toolbar {\n \nTool...
url
0 https://developer.apple.com/videos/play/wwdc20...
1 https://developer.apple.com/videos/play/wwdc20...
2 https://developer.apple.com/videos/play/wwdc20...
3 https://developer.apple.com/videos/play/wwdc20...
4 https://developer.apple.com/videos/play/wwdc20... )It looks like your current approach should work to trigger openImmersiveSpace
when connectionStatusViewModel.hasBeenConnected
changes. However, if the hasBeenConnected
property is not changing as expected, here are a few steps to troubleshoot and ensure proper functionality:
Ensure
hasBeenConnected
is being updated properly: Verify that theControllerConnectionStatusViewModel
class is correctly updating thehasBeenConnected
property. There might be an issue in the logic where this property is set.Check for SwiftUI updates: Sometimes, SwiftUI views might not update as expected if the properties are not marked correctly. Ensure that
hasBeenConnected
is a@Published
property in yourControllerConnectionStatusViewModel
.Test with a simplified version: Simplify your view to isolate the issue. Ensure that the
onChange
modifier is correctly triggering with a simpler condition.
Here is a revised version of your code with additional checks and simplifications:
import SwiftUI
import RealityKit
import RealityKitContent
class ControllerConnectionStatusViewModel: ObservableObject {
@Published var hasBeenConnected: Bool = false
}
struct ContentView: View {
@EnvironmentObject var connectionStatusViewModel: ControllerConnectionStatusViewModel
@Environment(\.openImmersiveSpace) var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace
var body: some View {
VStack {
ControllerConnectionStatusView(connectionStatusViewModel: connectionStatusViewModel)
GeometryReader { proxy in
let textWidth = min(max(proxy.size.width * 0.4, 300), 500)
ZStack {
HStack(spacing: 60) {
VStack(alignment: .leading, spacing: 0) {
Text("Laser Sword Trainer \(connectionStatusViewModel.hasBeenConnected)")
.font(.system(size: 50, weight: .bold))
.padding(.bottom, 15)
.onChange(of: connectionStatusViewModel.hasBeenConnected) { oldValue, newValue in
print("onChange happened \(oldValue) \(newValue)")
if oldValue == false && newValue {
Task {
await openImmersiveSpace(id: "TrainerSpace")
}
}
}
}
}
}
}
}
}
}
Additionally, you can place some debug prints or use breakpoints to ensure the property is being updated correctly in the ViewModel.
If you continue to face issues, please check for any warnings or errors in the console, as they might provide more context on what's going wrong. If the issue is still not resolved, it might be helpful to see the implementation of the ControllerConnectionStatusViewModel
class to ensure that it conforms to the expected behavior.//
// ControllerConnectionStatusViewModel.swift
// LaserSwordTrainer
//
// Created by Austin King on 6/13/24.
//
import Foundation import Combine
@MainActor final class ControllerConnectionStatusViewModel: ObservableObject { private var multipeerManager: MultipeerManager private var cancellables = Set()
@Published var hasBeenConnected: Bool = false {
didSet {
print("hasBeenConnected changed to \(hasBeenConnected)")
}
}
@Published var isConnected: Bool = false {
didSet {
print("isConnected changed to \(isConnected)")
}
}
init(multipeerManager: MultipeerManager) {
self.multipeerManager = multipeerManager
self.multipeerManager.$hasBeenConnected
.assign(to: \.hasBeenConnected, on: self)
.store(in: &cancellables)
self.multipeerManager.$isConnected
.assign(to: \.isConnected, on: self)
.store(in: &cancellables)
}
}
I do see hasBeenConnected changed to true
in the logsYour ControllerConnectionStatusViewModel
seems correctly implemented, and the logs indicate that hasBeenConnected
is indeed being updated. Given that you see the log message, the issue might be related to the timing or the way the SwiftUI view observes this change.
Here are a few things you can try:
Ensure ViewModel is correctly provided to the environment: Make sure that the
ControllerConnectionStatusViewModel
is correctly initialized and provided as an environment object in your app's entry point.Check if
openImmersiveSpace
is correctly called: Ensure theopenImmersiveSpace
function is available and correctly implemented.Simplify the view structure: Try simplifying your view to isolate the issue. Sometimes nested views or complex hierarchies can interfere with updates.
Explicitly test the
onChange
handler: Place a breakpoint or print statement inside theonChange
handler to verify it is triggered as expected.
Here is a slightly modified and simplified version of your code to help debug:
Main ContentView
import SwiftUI
import RealityKit
import RealityKitContent
struct ContentView: View {
@EnvironmentObject var connectionStatusViewModel: ControllerConnectionStatusViewModel
@Environment(\.openImmersiveSpace) var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace
var body: some View {
VStack {
ControllerConnectionStatusView(connectionStatusViewModel: connectionStatusViewModel)
GeometryReader { proxy in
let textWidth = min(max(proxy.size.width * 0.4, 300), 500)
ZStack {
HStack(spacing: 60) {
VStack(alignment: .leading, spacing: 0) {
Text("Laser Sword Trainer \(connectionStatusViewModel.hasBeenConnected)")
.font(.system(size: 50, weight: .bold))
.padding(.bottom, 15)
.onChange(of: connectionStatusViewModel.hasBeenConnected) { oldValue, newValue in
print("onChange triggered with oldValue: \(oldValue), newValue: \(newValue)")
if oldValue == false && newValue {
Task {
await openImmersiveSpace(id: "TrainerSpace")
}
}
}
}
}
}
}
}
}
}
App Entry Point
Ensure your App
struct correctly provides the ControllerConnectionStatusViewModel
:
import SwiftUI
@main
struct LaserSwordTrainerApp: App {
var multipeerManager = MultipeerManager()
var connectionStatusViewModel: ControllerConnectionStatusViewModel {
ControllerConnectionStatusViewModel(multipeerManager: multipeerManager)
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(connectionStatusViewModel)
}
}
}
This setup should ensure that the ControllerConnectionStatusViewModel
is properly provided to the ContentView
and that the onChange
modifier correctly responds to changes in the hasBeenConnected
property. If you still encounter issues, it might be helpful to review the MultipeerManager
class to ensure it behaves as expected.The debug line onChange triggered with oldValue...
never fires... Should I call .onChange
somewhere else in the view instead of on the Text
?If the onChange
handler isn't firing, it suggests that the SwiftUI view hierarchy isn't properly observing the change. You can try moving the onChange
handler to a higher level in your view hierarchy to ensure it captures the change in hasBeenConnected
.
Here's an updated approach where the onChange
handler is moved to a higher level in the view hierarchy:
import SwiftUI
import RealityKit
import RealityKitContent
struct ContentView: View {
@EnvironmentObject var connectionStatusViewModel: ControllerConnectionStatusViewModel
@Environment(\.openImmersiveSpace) var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace
var body: some View {
VStack {
ControllerConnectionStatusView(connectionStatusViewModel: connectionStatusViewModel)
GeometryReader { proxy in
let textWidth = min(max(proxy.size.width * 0.4, 300), 500)
ZStack {
HStack(spacing: 60) {
VStack(alignment: .leading, spacing: 0) {
Text("Laser Sword Trainer \(connectionStatusViewModel.hasBeenConnected)")
.font(.system(size: 50, weight: .bold))
.padding(.bottom, 15)
}
}
}
}
}
.onChange(of: connectionStatusViewModel.hasBeenConnected) { oldValue, newValue in
print("onChange triggered with oldValue: \(oldValue), newValue: \(newValue)")
if oldValue == false && newValue {
Task {
await openImmersiveSpace(id: "TrainerSpace")
}
}
}
}
}
By moving the onChange
handler to the VStack
, it ensures that any changes to hasBeenConnected
are observed at a higher level, potentially resolving the issue.
Additional Debugging Tips
Check for ViewModel Updates: Ensure that the
ControllerConnectionStatusViewModel
is properly updated and provided to theContentView
.Simplify Further: Temporarily remove other parts of the view to isolate the problem.
Verify Environment Object: Ensure that the environment object is properly injected:
Entry Point Verification
Ensure that your App
struct correctly provides the environment object:
import SwiftUI
@main
struct LaserSwordTrainerApp: App {
var multipeerManager = MultipeerManager()
var connectionStatusViewModel = ControllerConnectionStatusViewModel(multipeerManager: multipeerManager)
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(connectionStatusViewModel)
}
}
}
This setup should ensure that the ControllerConnectionStatusViewModel
is properly provided to the ContentView
and that the onChange
modifier correctly responds to changes in the hasBeenConnected
property.
Related Chats
SwiftUI View with Model 0.514
Fix Warning: Use StateObject 0.494
StateObject Error Resolution 0.482
Control Window Size: VisionOS 0.476
New chat 0.456
Quit App Button Swift 0.456
Add Function to SwiftUI. 0.449