Model3D: Getting Started

Loading models from bundles and URLs. Showing progress views and errors.

Overview

This is the first post in a short series of examples covering Model3D. We’ll start by learning how to load models, check phase for progress and errors, and add modifiers to the resolved model. Let’s load some model from the Reality Composer Pro content library. We already added these files to our bundle when we used them on some previous examples. We can use realityKitContentBundle and the name of the model we want to load.

Model3D(named: "Moon", bundle: realityKitContentBundle) { model in
    model
} placeholder: {
    ProgressView()
}

This will show a progress view while visionOS loads the asset. Once it has loaded, the model will be resolved and we can start working with it. We can use SwiftUI modifiers to customize it as needed. Let’s make it resizable, then make it fit within a frame.

Model3D(named: "Moon", bundle: realityKitContentBundle) { model in
    model
        .resizable()
        .scaledToFit3D()
} placeholder: {
    ProgressView()
}
.frame(width: 150, height: 150)

Learn more about scaling views.

If we need a bit more control, we can use this version of the initializer to check the phase of the loading process. We’ll use this approach with most of our examples. We can provide a progress view just like above, and we can also show a view when an error occurs.

Model3D(named: "ToyRocket", bundle: realityKitContentBundle) { phase in
    if let model = phase.model {
        model
            .resizable()
            .scaledToFit3D()
    } else if phase.error != nil {
        Text("Failed to load model")
    } else {
        ProgressView()
    }
}
.frame(width: 150, height: 150)

Loading a model from a URL is simple. Instead of using a name and a bundle, we just provide a valid URL with our USDZ file.

Model3D(url: URL(string: "https://stepinto.vision/wp-content/uploads/2025/10/Earth.usdz")!) { phase in

    if let model = phase.model {
        model
            .resizable()
            .scaledToFit3D()
    } else if phase.error != nil {
        Text("Failed to load model")
    } else {
        ProgressView()
    }
}
.frame(width: 150, height: 150)

Let’s make sure our logic works by forcing an error. We’ll provide a name for an asset that does not exist.

Model3D(named: "NotFound", bundle: realityKitContentBundle) { phase in
    if let model = phase.model {
        model
            .resizable()
            .scaledToFit3D()
    } else if phase.error != nil {
        Text("Failed to load model")
            .font(.caption)
            .padding()
            .background(.red)
            .clipShape(.capsule)
    } else {
        ProgressView()
    }
}
.frame(width: 150, height: 150)

See also

Video Demo

Full Example Code

struct Example113: View {
    var body: some View {
        HStackLayout(spacing: 12).depthAlignment(.front) {

            // Demo 01: simply load a model
            VStackLayout(spacing: 12).depthAlignment(.front) {

                Model3D(named: "Moon", bundle: realityKitContentBundle) { model in
                    model
                        .resizable()
                        .scaledToFit3D()

                } placeholder: {
                    ProgressView()
                }
                .frame(width: 150, height: 150)

                Text("Basic Model3D")
                    .font(.caption)
                    .padding()
                    .background(.black)
                    .clipShape(.capsule)
            }
            .offset(y: -50)

            // Demo 02: Using Model3DPhase to check progress and errors
            VStackLayout(spacing: 12).depthAlignment(.front) {
                Model3D(named: "ToyRocket", bundle: realityKitContentBundle) { phase in
                    if let model = phase.model {
                        model
                            .resizable()
                            .scaledToFit3D()
                    } else if phase.error != nil {
                        Text("Failed to load model")
                    } else {
                        ProgressView()
                    }
                }
                .frame(width: 150, height: 150)
                Text("Loading with Phase")
                    .font(.caption)
                    .padding()
                    .background(.black)
                    .clipShape(.capsule)
            }

            // Demo 03: Loading a model from a URL
            VStackLayout(spacing: 12).depthAlignment(.front) {
                Model3D(url: URL(string: "https://stepinto.vision/wp-content/uploads/2025/10/Earth.usdz")!) { phase in

                    if let model = phase.model {
                        model
                            .resizable()
                            .scaledToFit3D()
                    } else if phase.error != nil {
                        Text("Failed to load model")
                    } else {
                        ProgressView()
                    }
                }
                .frame(width: 150, height: 150)
                Text("Loading from a URL")
                    .font(.caption)
                    .padding()
                    .background(.black)
                    .clipShape(.capsule)
            }
            .offset(y: -50)

            // Demo 04: Forcing an error
            VStackLayout(spacing: 12).depthAlignment(.front) {
                // force an error by using a name for a model that does not exist
                Model3D(named: "NotFound", bundle: realityKitContentBundle) { phase in
                    if let model = phase.model {
                        model
                            .resizable()
                            .scaledToFit3D()
                    } else if phase.error != nil {
                        Text("Failed to load model")
                            .font(.caption)
                            .padding()
                            .background(.red)
                            .clipShape(.capsule)
                    } else {
                        ProgressView()
                    }
                }
                .frame(width: 150, height: 150)
                Text("Error Handling")
                    .font(.caption)
                    .padding()
                    .background(.black)
                    .clipShape(.capsule)
            }
        }
    }
}

Support our work so we can continue to bring you new examples and articles.

Download the Xcode project with this and many more examples from Step Into Vision.
Some examples are provided as standalone Xcode projects. You can find those here.

Questions or feedback?