hi, i need some help with my ios app crashing
# ask-ai
p
hi, i need some help with my ios app crashing
t
Thanks for reaching out, adit-kulkarni. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’m sorry your iOS app is crashing. To help narrow this down, can you answer a few quick questions about the crash? • When does the crash happen (app launch, joining a room, publishing track, backgrounding, specific UI action)? • Is this on a physical device or simulator? Device model and iOS version? • Are you using the LiveKit iOS SDK? If so, which SDK version? • Do you have crash logs / console output or an Xcode crash report (stack trace)? If yes, please paste the relevant lines or upload the report. • Any recent code or dependency changes (WebRTC version, plugins, permission code)? • Do you see any runtime errors about missing plugins, permissions, or audio/video initialization? Share those details and I’ll suggest focused troubleshooting steps (symbolication, reproducer, common LiveKit causes and fixes). Sources: [Flutter iOS] SoundWaveformWidget / AudioVisualizerWidget Not Working (Miss...
p
this is on my physical iphone device. iphone 15 pro max. here is some info: Crash Symptoms - App crashes when starting a second LiveKit conversation after exiting the first - First conversation works fine, exit flow completes, then starting another conversation crashes - Logs show cleanup() being called twice before crash, then immediate deinit of manager Current Code Structure struct LiveKitConversationView: View { @StateObject private var session: Session @StateObject private var localMedia: LocalMedia @StateObject private var conversationManager: LiveKitConversationManager init(settings: ConversationSettings) { // Creates Session and LocalMedia synchronously in init let tokenSource = SaraSpeakTokenSource().cached() let session = Session(tokenSource: tokenSource, options: ...) _session = StateObject(wrappedValue: session) _localMedia = StateObject(wrappedValue: LocalMedia(session: session)) } } The view is presented via .fullScreenCover(isPresented:). Suspected Root Cause SwiftUI can call the view's init multiple times during fullScreenCover dismiss animations. This creates multiple Session and LocalMedia instances that may conflict with each other or with LiveKit SDK internal state. Proposed Fix Move Session/LocalMedia creation out of init into a lazy start() method called from onAppear, so expensive objects are only created once when actually needed (not during animation recreations). Question for LiveKit Is it safe to create multiple Session or LocalMedia instances in quick succession? Are there any singleton/global resources in the SDK that could cause conflicts when a Session is created, immediately destroyed, then another created?
t
Thanks — this is very helpful detail. You're correct: the crash behavior and logs (cleanup() called twice, immediate deinit) strongly indicate a lifecycle/race issue with creating and destroying LiveKit sessions and local media rapidly from SwiftUI. Quick answers to your explicit questions • Is it safe to create multiple Session or LocalMedia instances in quick succession? No — creating and destroying Session/LocalMedia instances rapidly is not safe. The LiveKit iOS SDK relies on shared native resources (WebRTC, AVFoundation audio/video capture, and internal delegates) that can conflict if a new session is created before the prior one is fully torn down. • Are there singleton/global resources that could cause conflicts? Yes — the WebRTC engine, audio/video capture contexts, and internal callback/delegate wiring are effectively shared and can cause race conditions, incomplete deallocation, or crashes when instances overlap. What to do next (concise recommended changes) 1. Delay initialization — move creation of
Session
and
LocalMedia
out of the View init and into a controlled start flow (e.g.,
start()
called from
onAppear
) so objects are only created once per appearance. 2. Ensure orderly async teardown — on exit call your disconnect/cleanup and await its completion before releasing instances: ◦ call
await session.disconnect()
(or equivalent cleanup API), ◦ then nil out `localMedia`/`session`. 1. Guard against double starts — add an `isInitialized`/`isStarting` flag or manager-level guard so init is only performed once while animations or view re-inits occur. 2. Use a manager/service for lifecycle — encapsulate session creation/teardown in a single
LiveKitConversationManager
(ObservableObject) rather than creating Session/LocalMedia directly in the View init. This centralizes guards and cleanup. 3. Reset state on complete teardown — after disconnect completes, set the flag so a subsequent conversation can start cleanly. Example pattern (concept)
Copy code
@StateObject private var manager = LiveKitConversationManager()
@State private var isInitialized = false

.onAppear {
  if !isInitialized {
    isInitialized = true
    manager.start()
  }
}
.onDisappear {
  Task {
    await manager.stop() // await full disconnect and cleanup
    isInitialized = false
  }
}
Additional safeguards if crashes persist • Ensure you wait for the SDK disconnect callback to finish before allowing a new start. • Add logging around cleanup start/completion to confirm no overlapping lifecycles. • If you keep seeing double
cleanup()
calls, trace what triggers cleanup (view lifecycle vs manager) and ensure only one owner calls teardown. If you’d like, paste the key parts of your cleanup/disconnect code (or the manager implementation) and I’ll point out where to add guards/waits or suggest precise code changes. Sources: client-sdk-flutter | livekit/livekit_composite | DeepWiki | client-sdk-swift | livekit/livekit_composite | DeepWiki
Thanks — this is very helpful detail. I’m responding to your logs and current behavior (crash when starting a second conversation, cleanup called twice, manager re-initialized). What the logs show • Your manager logs show a normal stop flow: "Stopping session...", "Session ended", "Post-disconnect delay complete", "Cleanup complete". • Immediately after cleanup the UI proceeds to character selection and then initializes/starts a new session ("Initialized" → "onAppear - starting session" → "Starting session..."). • The orientation transaction log and SwiftUI fullScreenCover dismissal suggest SwiftUI is re-instantiating views during the dismiss animation — that can trigger multiple lifecycle events. • The entitlement log "container_create_or_lookup_app_group_path_by_app_group_identifier: client is not entitled" is unrelated to LiveKit lifecycling crashes but worth fixing separately if you use app groups. Why this can still crash • Even though your cleanup finished, native WebRTC/AVFoundation resources can be still winding down at the OS/native layer after your SDK-level cleanup completes. Starting a new Session too quickly can overlap native teardown/startup and cause races/crashes. • The double cleanup call you saw earlier likely means two owners/paths are attempting teardown (e.g., view onDisappear + manager deinit). That increases the chance of racing the native teardown. Immediate recommendations (small, focused changes) 1. Enforce a short safety delay after cleanup before allowing a new start. ◦ After disconnect + cleanup, wait a small window (200–500 ms) before creating a new Session. This avoids overlapping native teardown/start. Example (concept):
Copy code
await session.disconnect()
   // ensure cleanup finished
   try? await Task.sleep(nanoseconds: 300_000_000) // 300ms
   // now allow new session creation
1. Make teardown idempotent and single-owner: ◦ Ensure only one component owns cleanup (preferably the manager). Have onDisappear call manager.stop() but avoid duplicate stop calls from deinit or elsewhere. ◦ Add a guard flag inside stop(): if isStopping { return } and set/clear it appropriately. 1. Add start guard to prevent double starts: ◦ If isStarting or isInitialized, ignore additional start attempts until start completes (or fails). ◦ Use Task cancellation or a mutex to serialize start/stop. 1. Await disconnect completion before navigating: ◦ If your UI navigates away as soon as stop() is called, ensure you await stop completion or wait for manager state transition before creating/presenting the next conversation. 1. Add verbose lifecycle logs around start/stop: ◦ Log timestamps for start requested, start completed, stop requested, stop completed, and manager deinit so you can confirm there’s no overlap. Additional checks • Paste the actual Xcode crash report (stack trace) if you have one — that will show whether the crash is in WebRTC native code, LiveKit callbacks, or your app code. • Confirm you moved Session/LocalMedia creation into start() and not the View init. Also confirm stop() calls
await session.disconnect()
and then nils the references. • If you use AVAudioSession modifications, ensure you restore/teardown audio session states in a consistent place (manager.stop()). If you want, paste your manager’s start/stop implementations (the functions that create Session/LocalMedia and call disconnect/cleanup). I’ll point out exactly where to add the guards, delay, and idempotency checks.
p
but then across conversations, how do i clear messages? i dont want the previous conversation's messages to appear in a new convo
t
You're right to separate message state from the LiveKit session lifecycle — LiveKit won't automatically clear your UI message list. To ensure a new conversation starts with a clean slate, clear or reset the message store when ending the old session or before starting the new one. Quick steps you can apply now 1. Identify where messages are stored (ViewModel, manager,
@StateObject
, singleton). 2. Clear the store on end or before start: ◦ On disconnect/cleanup:
messages.removeAll()
◦ Or just before calling
manager.start()
for the new conversation. 1. Tie clearing to lifecycle events: ◦ After
await session.disconnect()
or
RoomEvent.disconnected
, call
messages.removeAll()
. ◦ Or in your manager’s
stop()
/
start()
methods: clear on stop; initialize empty on start. 1. If messages are shared globally, scope them per session ID: ◦ Use a dictionary
[sessionId: [Message]]
and switch to an empty array for a new session. 1. Update the UI reactively so the view reflects the cleared array immediately. If you paste where you currently hold messages (ViewModel/manager code or
@StateObject
), I’ll show the exact lines to add to safely clear state during your start/stop flow. Sources: Text and transcriptions | LiveKit docs