Sample CodeiOS, iPadOS, Mac Catalyst, macOS, visionOSReviewed 2026-11-01View on Apple Developer

Rendering Gaussian splats with RealityKit

At a glance

Item Summary
Purpose Load a Gaussian splat from a bundled PLY or USD file into LowLevelBuffers and render it with GaussianSplatComponent. On visionOS it appears in a mixed immersive space. On iOS and macOS it appears in a window.
App architecture One SwiftUI app with a small @Observable AppModel (loadedAsset, immersiveSpaceState). Compile-time #if os(...) picks the presentation: SpatialSplatView in an ImmersiveSpace on visionOS, or WindowSplatView inside ContentView on iOS and macOS. Both views call the same free-function loaders and entity builder.
Main patterns SwiftUI environment injection; observable state; SwiftUI–RealityKit bridge; platform-conditional view variants
Project style One multiplatform target. Ten flat Swift files in RealityKit-GaussianSplats/. Platform-only files are wrapped entirely in #if.
Execution model The target defaults to main-actor isolation. Parsing runs inside an awaited Task.detached that calls nonisolated functions. Entity construction is @MainActor. ARKit anchor updates are consumed by a SwiftUI .task.
State/event model Swift Observation on AppModel. .task(id: appModel.loadedAsset) triggers loads. RealityView update: syncs @State splatEntity into the entity graph. A retained EventSubscription handles manipulation release.
Key frameworks/packages RealityKit, SwiftUI, ARKit (visionOS only), USDKit, Metal (device builds), Foundation, os. No package manifest.

Platform boundary: Apple’s documentation page lists Mac Catalyst. The verified project declares SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator" and has no Mac Catalyst setting (RealityKit-GaussianSplats.xcodeproj/project.pbxproj:317). The code branches on os(macOS) || os(iOS) (RealityKit-GaussianSplats/WindowSplatView.swift:15). Front-matter platforms follow the official listing. Every macOS statement below is grounded in the native macosx destination.

Project structure

RealityKit-GaussianSplats/
├── RealityKitGaussianSplatsApp.swift   // scenes; ImmersiveSpace only on visionOS
├── AppModel.swift                      // @Observable shared state
├── ContentView.swift                   // two #if variants (visionOS / iOS+macOS)
├── SpatialSplatView.swift              // #if os(visionOS): immersive splat + snapping
├── SceneReconstructionManager.swift    // #if os(visionOS): ARKit meshes → collision
├── WindowSplatView.swift               // #if os(macOS) || os(iOS): turntable window
├── PLYLoader.swift                     // loadPLYEntity (Task.detached)
├── PLYParser.swift                     // nonisolated PLY parse + deinterleave
├── USDLoader.swift                     // loadUSDEntity via USDKit (Task.detached)
└── SplatEntityBuilder.swift            // GaussianSplatBuffers, makeSplatEntity
RealityKit-GaussianSplats.xcodeproj/
Configuration/SampleCode.xcconfig

Structure observations

  • The files fall into two layers. Presentation is App, ContentView, SpatialSplatView, and WindowSplatView. Loading is PLYLoader, PLYParser, USDLoader, and SplatEntityBuilder. The loading layer has no types beyond value structs: its API is free functions such as loadPLYEntity(assetName:) and makeSplatEntity(from:isLinear:).
  • Platform separation happens at the file level. SpatialSplatView.swift and SceneReconstructionManager.swift are entirely inside #if os(visionOS). WindowSplatView.swift is entirely inside #if os(macOS) || os(iOS) (RealityKit-GaussianSplats/SpatialSplatView.swift:16, RealityKit-GaussianSplats/SceneReconstructionManager.swift:13, RealityKit-GaussianSplats/WindowSplatView.swift:15). ContentView.swift holds two complete ContentView declarations separated by #else (RealityKit-GaussianSplats/ContentView.swift:13, RealityKit-GaussianSplats/ContentView.swift:67).
  • Simulator builds compile throwing stubs instead of the GPU path: makeSplatEntity and loadUSDEntity sit behind #if targetEnvironment(simulator) (RealityKit-GaussianSplats/SplatEntityBuilder.swift:43, RealityKit-GaussianSplats/USDLoader.swift:25).
  • The project uses SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor with SWIFT_VERSION = 5.0 (RealityKit-GaussianSplats.xcodeproj/project.pbxproj:319, RealityKit-GaussianSplats.xcodeproj/project.pbxproj:322). This is why parser functions are marked nonisolated.

Overall architecture

Reference code

RealityKit-GaussianSplats/RealityKitGaussianSplatsApp.swift:13 — RealityKitGaussianSplatsApp

struct RealityKitGaussianSplatsApp: App {
    @State private var appModel = AppModel()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(appModel)
        }
        // ...
        #if os(visionOS)
        ImmersiveSpace(id: appModel.immersiveSpaceId) {
            SpatialSplatView()
                .environment(appModel)
        }
        .immersionStyle(selection: .constant(.mixed), in: .mixed)
        #endif
    }
}

Interpretation

Observation: buttons never call a loader directly. They set AppModel.loadedAsset. The visible splat view reacts with .task(id: appModel.loadedAsset), clears its current entity, and loads the new one (RealityKit-GaussianSplats/SpatialSplatView.swift:100, RealityKit-GaussianSplats/WindowSplatView.swift:100). On visionOS, ContentView.load(_:) first opens the immersive space and sets loadedAsset only after .opened (RealityKit-GaussianSplats/ContentView.swift:42).

Inference: one enum value is the whole contract between the control UI and the renderer. That lets both platform views reuse the same trigger without a shared view model.

Ownership and state

Ownership evidence

RealityKit-GaussianSplats/SpatialSplatView.swift:40 — state that must outlive view rebuilds

final class SpatialSplatViewCoordinator {
    let rootEntity = Entity()
    let sceneManager = SceneReconstructionManager()
    let spotLight = Entity()
    var releaseSubscription: EventSubscription?
    var attachedSplat: Entity?
    // ...
}

RealityKit-GaussianSplats/SpatialSplatView.swift:81 — the view stores it in @State

struct SpatialSplatView: View {
    @Environment(AppModel.self) private var appModel
    @State private var coordinator = SpatialSplatViewCoordinator()
    @State private var splatEntity: Entity?
    // ...
}
Owner Object or state Relationship Mutation authority
RealityKitGaussianSplatsApp AppModel Creates in @State. Injects into both scenes (RealityKit-GaussianSplats/RealityKitGaussianSplatsApp.swift:14) —
ContentView (both variants) AppModel.loadedAsset, .immersiveSpaceState Reads from the environment and assigns directly (RealityKit-GaussianSplats/ContentView.swift:56, RealityKit-GaussianSplats/ContentView.swift:81) ContentView is the only writer in source.
SpatialSplatView SpatialSplatViewCoordinator, splatEntity @State. SwiftUI keeps them across body re-evaluation, as the doc comment says (RealityKit-GaussianSplats/SpatialSplatView.swift:34-38). The view’s syncSplat and loadAsset
SpatialSplatViewCoordinator rootEntity, sceneManager, spotLight Creates as let (RealityKit-GaussianSplats/SpatialSplatView.swift:41-43) Immutable references. Entity transforms are mutated by the view.
SpatialSplatViewCoordinator releaseSubscription: EventSubscription? Retains the manipulation-release subscription. Set to nil before re-subscribing (RealityKit-GaussianSplats/SpatialSplatView.swift:113, RealityKit-GaussianSplats/SpatialSplatView.swift:127) SpatialSplatView.syncSplat
SceneReconstructionManager ARKitSession, SceneReconstructionProvider, meshEntities private stored state (RealityKit-GaussianSplats/SceneReconstructionManager.swift:38-40) The manager only
WindowSplatView WindowSplatViewCoordinator, splatEntity, spin/tilt @State (RealityKit-GaussianSplats/WindowSplatView.swift:74-84) The drag gesture and syncSplat

Note: the type names end in Coordinator, but the source describes them as holders for “RealityKit state that lives longer than one render of the view” (RealityKit-GaussianSplats/SpatialSplatView.swift:34, RealityKit-GaussianSplats/WindowSplatView.swift:52). They coordinate no navigation and no multi-object flow.

Concurrency, scheduling, and thread safety

Evidence limit: actor isolation, async/await, or Task creation does not by itself prove background-thread execution; Sendable conformance alone does not prove thread-safe mutation.

The target compiles with SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor (RealityKit-GaussianSplats.xcodeproj/project.pbxproj:319). Views, AppModel, both …Coordinator classes, SceneReconstructionManager, and the loader entry points are main-actor isolated, several also by explicit @MainActor. The parsing helpers opt out with nonisolated (RealityKit-GaussianSplats/PLYParser.swift:151, RealityKit-GaussianSplats/PLYParser.swift:285, RealityKit-GaussianSplats/USDLoader.swift:131).

Concern Source mechanism Verified placement or handoff Evidence
Main-actor isolation @MainActor on AppModel, SceneReconstructionManager, …Coordinator, loadPLYEntity, loadUSDEntity, makeSplatEntity Entity creation, GaussianSplatComponent assembly, and model state run on the main actor. RealityKit-GaussianSplats/AppModel.swift:11, RealityKit-GaussianSplats/SplatEntityBuilder.swift:111
Detached work try await Task.detached(priority: .userInitiated) { ... }.value PLY read, header parse, and AoS→SoA deinterleave run in a detached task that calls only nonisolated synchronous functions. It does not inherit the main actor. No specific thread is established. The main-actor caller suspends until .value, then builds the entity. RealityKit-GaussianSplats/PLYLoader.swift:29
Detached work (USD) Same shape USDStage.open, prim lookup, and buildBuffers run in a detached task. makeSplatEntity(from:isLinear: true) follows on the main actor. RealityKit-GaussianSplats/USDLoader.swift:41
Transfer contract struct GaussianSplatBuffers: @unchecked Sendable Not synchronization. The comment states the invariant: the loader writes the buffers in the background task, then nothing writes them after the handoff. The compiler does not check that invariant. RealityKit-GaussianSplats/SplatEntityBuilder.swift:21
Load trigger and cancellation SwiftUI .task(id: appModel.loadedAsset) SwiftUI owns this task and restarts it when loadedAsset changes. The detached child is not structured, so cancelling the .task does not cancel parsing. loadAsset() has no cancellation check before assigning splatEntity. RealityKit-GaussianSplats/SpatialSplatView.swift:100, RealityKit-GaussianSplats/SpatialSplatView.swift:135
Async sequence .task { await coordinator.sceneManager.run() }, then for await update in provider.anchorUpdates The view-scoped task runs authorization, session.run, and the anchor loop. Each applyAnchor is awaited before the next update, so meshEntities is mutated serially on the main actor. RealityKit-GaussianSplats/SpatialSplatView.swift:104, RealityKit-GaussianSplats/SceneReconstructionManager.swift:60
Event subscription content.subscribe(to: ManipulationEvents.WillRelease.self, on:) The returned EventSubscription is stored in the coordinator and released before re-subscribing for a new splat. The handler calls @MainActor snapToNearestSurface. RealityKit-GaussianSplats/SpatialSplatView.swift:127, RealityKit-GaussianSplats/SpatialSplatView.swift:166
UI tasks Plain Task { await load(.ply) } in buttons Created in a main-actor view context. Not stored or cancelled. The buttons are disabled while .inTransition. RealityKit-GaussianSplats/ContentView.swift:30, RealityKit-GaussianSplats/ContentView.swift:36

No DispatchQueue, lock, or custom actor appears in the tree. Mutable shared state is limited to main-actor objects. The off-main work exchanges only freshly allocated buffers, returned through .value.

Inference on ordering: suppose the user switches assets while a parse is still running. The old .task is cancelled, but its detached parse keeps going, and the old loadAsset() can still assign splatEntity when it finishes. The source shows no guard against a slower earlier load finishing last.

Reference code

RealityKit-GaussianSplats/PLYLoader.swift:26 — loadPLYEntity(assetName:)

@MainActor
func loadPLYEntity(assetName: String) async throws -> Entity {
    let url = try bundlePLYURL(assetName: assetName)
    let buffers = try await Task.detached(priority: .userInitiated) {
        let splatData = try readGaussianSplatFile(url)
        return try deinterleaveGaussianSplatData(splatData)
    }.value
    return try makeSplatEntity(from: buffers)
}

RealityKit-GaussianSplats/SpatialSplatView.swift:100 — view-scoped tasks

.task(id: appModel.loadedAsset) {
    splatEntity = nil
    await loadAsset()
}
.task {
    await coordinator.sceneManager.run()
}

State propagation, frameworks, and dependencies

Evidence limit: an import proves a source-level compilation dependency at the cited line; it does not prove runtime use, architectural adoption, or whether a Swift package is a direct application dependency.

Category Mechanism or module Verified role Evidence
State propagation @Observable AppModel plus @Environment(AppModel.self) loadedAsset changes restart the splat view’s load task. immersiveSpaceState gates buttons. RealityKit-GaussianSplats/AppModel.swift:35, RealityKit-GaussianSplats/ContentView.swift:16
State propagation @State splatEntity → RealityView update: syncSplat compares by identity (===) and attaches or detaches only when the entity changed. RealityKit-GaussianSplats/SpatialSplatView.swift:109, RealityKit-GaussianSplats/WindowSplatView.swift:145
State propagation DragGesture → @State spin/tilt → update: The turntable rotation is recomputed from view state on each update. The tilt is clamped. RealityKit-GaussianSplats/WindowSplatView.swift:121, RealityKit-GaussianSplats/WindowSplatView.swift:94
Framework import RealityKit, SwiftUI All presentation files and loaders RealityKit-GaussianSplats/SplatEntityBuilder.swift:10, RealityKit-GaussianSplats/AppModel.swift:9
Framework import ARKit, os visionOS-only files RealityKit-GaussianSplats/SceneReconstructionManager.swift:15, RealityKit-GaussianSplats/SpatialSplatView.swift:18
Framework import USDKit USD prim and attribute reads RealityKit-GaussianSplats/USDLoader.swift:11
Framework import Metal (non-simulator only) MTLAttributeFormat for buffer descriptors RealityKit-GaussianSplats/SplatEntityBuilder.swift:52

No Combine import, notification, or package manifest appears in the tree.

Class and protocol design

Reference code

RealityKit-GaussianSplats/SplatEntityBuilder.swift:111 — the shared construction point

@MainActor
func makeSplatEntity(from buffers: GaussianSplatBuffers, isLinear: Bool = false) throws -> Entity {
    let comp = try assembleSplatComponent(from: buffers, isLinear: isLinear)
    let entity = Entity()
    entity.components.set(comp)
    return entity
}
Type or file Responsibility Depends on
AppModel Asset selection and immersive-space phase SwiftUI (Observation)
ContentView (visionOS) Opens and dismisses the immersive space, then sets loadedAsset openImmersiveSpace, dismissImmersiveSpace
ContentView (iOS, macOS) Hosts WindowSplatView with overlay buttons WindowSplatView
SpatialSplatView + SpatialSplatViewCoordinator Immersive placement, spotlight, manipulation, raycast snap SceneReconstructionManager, loaders
WindowSplatView + WindowSplatViewCoordinator Virtual camera and turntable rotation in a window loaders
SceneReconstructionManager ARKit mesh anchors → static collision entities in collisionGroup ARKit, RealityKit
GaussianSplatBuffers Immutable SoA GPU buffers handed from parser to builder LowLevelBuffer
PLYLoader / PLYParser, USDLoader Format-specific parsing into GaussianSplatBuffers Foundation, USDKit
SplatEntityBuilder Descriptors, activations (exponential/sigmoid vs identity), entity RealityKit, Metal

No app-defined protocol exists. The two loaders converge on one value type and one builder function instead of sharing a loader protocol.

Access control

Symbol Access Verified effect Likely rationale
bundlePLYURL, bundleUSDURL (RealityKit-GaussianSplats/PLYLoader.swift:15, RealityKit-GaussianSplats/USDLoader.swift:16) file-scope private Callable only within each loader file Inference: the public surface per format is one load…Entity function.
SplatAttributes, loadSplatAttributes, buildBuffers, fill helpers (RealityKit-GaussianSplats/USDLoader.swift:58, RealityKit-GaussianSplats/USDLoader.swift:131) file-scope private (private nonisolated func) USD parsing internals are invisible to other files. Inference: keeps format details local.
PLYHeader (RealityKit-GaussianSplats/PLYParser.swift:59) private nonisolated struct Header parsing is file-local. readGaussianSplatFile and deinterleaveGaussianSplatData stay implicit internal. Inference: only the two entry points cross files.
SceneReconstructionManager.session, .provider, .meshEntities (RealityKit-GaussianSplats/SceneReconstructionManager.swift:38) private Only run() and applyAnchor touch the ARKit session and mesh map. rootEntity and collisionGroup are implicit internal for the view. Inference: expose the scene graph root and raycast mask, not the session.
splatScale, spotLightIntensity, cameraDistance, tilt limits (RealityKit-GaussianSplats/SpatialSplatView.swift:30, RealityKit-GaussianSplats/WindowSplatView.swift:31) file-scope private let Tuning constants are duplicated per file, not shared. Inference: each platform view tunes itself independently.
ContentView.load(_:), close() (RealityKit-GaussianSplats/ContentView.swift:42, RealityKit-GaussianSplats/ContentView.swift:59) private Only the view body calls them. Inference: the space transition is UI-local.
…Coordinator stored properties (RealityKit-GaussianSplats/SpatialSplatView.swift:44) implicit internal var The owning view mutates attachedSplat and releaseSubscription directly. Inference: the holder is a passive state box for its view, not an encapsulated service.

Reference code

RealityKit-GaussianSplats/SceneReconstructionManager.swift:29 — narrow internal surface

final class SceneReconstructionManager {
    // ...
    static let collisionGroup = CollisionGroup(rawValue: 1 << 2)

    let rootEntity = Entity()

    private let session = ARKitSession()
    private let provider = SceneReconstructionProvider()
    private var meshEntities: [UUID: Entity] = [:]
    // ...
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Space open → then select asset ContentView.load(_:) (visionOS) Sets loadedAsset only after .opened. Resets to .closed on cancel or error (RealityKit-GaussianSplats/ContentView.swift:43-56).
Asset → entity SpatialSplatView.loadAsset / WindowSplatView.loadAsset Identical switch in both views (RealityKit-GaussianSplats/SpatialSplatView.swift:135, RealityKit-GaussianSplats/WindowSplatView.swift:157)
Parsing and buffer layout PLYParser.swift, USDLoader.swift nonisolated pure functions, so they can run inside the detached task
GPU resource assembly SplatEntityBuilder.swift One place that knows descriptor strides and activation choice (RealityKit-GaussianSplats/SplatEntityBuilder.swift:99-105)
Surface snapping SpatialSplatView.snapToNearestSurface with SceneReconstructionManager.collisionGroup Raycasts up and down, picks the nearest hit, animates move(to:) (RealityKit-GaussianSplats/SpatialSplatView.swift:167-201)
Turntable math WindowSplatView.turntableRotation Spin before tilt, tilt clamped to 8°–80° (RealityKit-GaussianSplats/WindowSplatView.swift:138)

Design patterns

Pattern Source evidence Purpose or tradeoff
SwiftUI environment injection RealityKit-GaussianSplats/RealityKitGaussianSplatsApp.swift:19 and RealityKit-GaussianSplats/RealityKitGaussianSplatsApp.swift:33 One AppModel instance reaches the window and the immersive space.
Observable state RealityKit-GaussianSplats/AppModel.swift:12 @Observable class AppModel A tiny shared model. loadedAsset is the trigger that restarts the view’s .task(id:).
SwiftUI–RealityKit bridge RealityKit-GaussianSplats/SpatialSplatView.swift:94 (RealityView make/update plus @State holder) SwiftUI state (splatEntity, spin, tilt) is pushed into a persistent entity graph held in a reference type, so rebuilding the view doesn’t recreate entities.
Platform-conditional view variants RealityKit-GaussianSplats/ContentView.swift:13 and RealityKit-GaussianSplats/WindowSplatView.swift:15 Same type name (ContentView) and shared loaders, with platform presentation chosen at compile time. The tradeoff is duplicated loadAsset and constants.

Removed from the draft: Coordinator. The draft inferred it from the SpatialSplatViewCoordinator type name. The source documents these classes as state holders kept in @State across view rebuilds (RealityKit-GaussianSplats/SpatialSplatView.swift:34-38). They coordinate no navigation, flow, or collaborators.

Not claimed: Builder. SplatEntityBuilder.swift contains free functions that construct in one call, not an incremental builder object. Also not claimed: protocol-oriented design, since no app protocol exists.

Main application flow

Reference code

RealityKit-GaussianSplats/SpatialSplatView.swift:109 — syncSplat(into:)

    private func syncSplat(into content: RealityViewContent) {
        if coordinator.attachedSplat === splatEntity { return }

        coordinator.attachedSplat?.removeFromParent()
        coordinator.releaseSubscription = nil
        coordinator.attachedSplat = nil

        guard let splatEntity else { return }

        configureManipulableObject(splatEntity)
        // ...
        coordinator.attachedSplat = splatEntity
        coordinator.releaseSubscription = content.subscribe(
            to: ManipulationEvents.WillRelease.self,
            on: splatEntity
        ) { event in
            snapToNearestSurface(event.entity)
        }
    }

Naming conventions

  • Types: the Spatial… and Window… prefixes name the presentation surface (immersive space or window), not the platform. …ViewCoordinator names a view-owned reference holder (see the note above). …Manager owns an ARKit session. AppModel is the shared observable model. GaussianSplatBuffers and GaussianSplatData name data shapes. GaussianSplatError.invalidData(String) is the single error case.
  • Functions: load…Entity(assetName:) is the async loader entry point. make… builds a value (makeSplatEntity, makeDescriptor, makeSHDescriptor). read…, parse…, deinterleave…, fill…Buffer, and copyPLYField are pipeline steps. configure… mutates an entity in place (configureSpotLight, configureManipulableObject). sync… reconciles state into the scene (syncSplat).
  • State: immersiveSpaceState with closed/inTransition/open. loadedAsset with unloaded/ply/usd. settledSpin/spin distinguish the committed value from the in-drag value.
  • Files: one primary type or format per file. SplatEntityBuilder.swift names a role but holds free functions. Rename from the earlier sample: SplatView → SpatialSplatView, SplatViewCoordinator → SpatialSplatViewCoordinator, GaussianSplatsOnVisionOSApp → RealityKitGaussianSplatsApp.

Architecture takeaways

  • Use one observable enum (loadedAsset) as the contract between controls and renderer. .task(id:) turns each change into a restartable load without a dedicated view model.
  • Keep RealityKit objects in a reference type stored in @State, and reconcile in RealityView’s update: by identity. Rebuilding the view then never recreates entities or subscriptions.
  • Run CPU-heavy parsing as nonisolated functions inside an awaited Task.detached, and hand back an immutable buffer struct. @unchecked Sendable documents an invariant, it doesn’t enforce one, and detached work isn’t cancelled with the caller.
  • Add platforms with whole-file #if boundaries and a same-named ContentView per branch. The loading layer stays shared, at the cost of small duplicated view logic.
  • Read type suffixes against behavior. Here Coordinator means “view-lifetime state holder”, not the Coordinator pattern.

Source map

Source file Relevant symbols
RealityKit-GaussianSplats/RealityKitGaussianSplatsApp.swift RealityKitGaussianSplatsApp, ImmersiveSpace (visionOS)
RealityKit-GaussianSplats/AppModel.swift AppModel, ImmersiveSpaceState, LoadedAsset
RealityKit-GaussianSplats/ContentView.swift ContentView (two variants), load(_:), close()
RealityKit-GaussianSplats/SpatialSplatView.swift SpatialSplatViewCoordinator, SpatialSplatView, syncSplat, snapToNearestSurface, SnapOffset
RealityKit-GaussianSplats/WindowSplatView.swift WindowSplatViewCoordinator, WindowSplatView, dragGesture, turntableRotation
RealityKit-GaussianSplats/SceneReconstructionManager.swift SceneReconstructionManager, run(), applyAnchor, collisionGroup
RealityKit-GaussianSplats/PLYLoader.swift loadPLYEntity, Task.detached
RealityKit-GaussianSplats/PLYParser.swift readGaussianSplatFile, deinterleaveGaussianSplatData, PLYHeader
RealityKit-GaussianSplats/USDLoader.swift loadUSDEntity, buildBuffers, SplatAttributes
RealityKit-GaussianSplats/SplatEntityBuilder.swift GaussianSplatBuffers, assembleSplatComponent, makeSplatEntity
RealityKit-GaussianSplats.xcodeproj/project.pbxproj SUPPORTED_PLATFORMS, SWIFT_DEFAULT_ACTOR_ISOLATION, SWIFT_VERSION