SCNParticleSystem load from document Directory

Viewed 438

I am trying load SCNParticleSystem from download bundle which i am not able to load.

Path for the resource.

file:///var/mobile/Containers/Data/Application/A91E9970-CDE1-43D8-B822-4B61EFC6149B/Documents/so/solarsystem.bundle/Contents/Resources/

                let objScene = SCNParticleSystem(named: "stars", inDirectory: directory)

This object is nil.

3 Answers

This is a legitimate problem since SceneKit does not provide an out-of-the-box solution for initializing particle systems from files that are outside of the main bundle (the only init method SCNParticleSystem.init(named:inDirectory:) implies that SCNParticleSystem.scnp files are in the main bundle).

Luckily for us .scnp files are just encoded/archived SCNParticleSystem instances that we can easily decode/unarchive using NSKeyedUnarchiver:

extension SCNParticleSystem {
    static func make(fromFileAt url: URL) -> SCNParticleSystem? {
        guard let data = try? Data(contentsOf: url),
            let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
            let system = object as? SCNParticleSystem else { return nil }

        return system
    }
}

If you do not need to support iOS 9 and iOS 10 you can use NSKeyedUnarchiver.unarchivedObject(ofClass: SCNParticleSystem.self, from: data) instead of NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(_:) and type casting, which was introduced in iOS 11.0.

Another issue that you're most likely to encounter is missing particle images. That is because by default SceneKit will look for them in the main bundle. As of current versions of iOS (which is iOS 12) and Xcode (Xcode 10) particle images in .scnp files (particleImage property) are String values which are texture filenames in the main bundle (that might change, but probably won't, however there's not much else we could use).

So my suggestion is to take that filename and look for the texture file with the same name in the same directory where the .scnp file is:

extension SCNParticleSystem {
    static func make(fromFileAt url: URL) -> SCNParticleSystem? {
        guard let data = try? Data(contentsOf: url),
            let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
            let system = object as? SCNParticleSystem else { return nil }

        if let particleImageName = system.particleImage as? String {
            let particleImageURL = url
                .deletingLastPathComponent()
                .appendingPathComponent(particleImageName)

            if FileManager.default.fileExists(atPath: particleImageURL.path) {
                system.particleImage = particleImageURL
            }
        }

        return system
    }
}

You can just set the URL of the image file and SceneKit will handle it from there.


As a little side-note, the recommended directory for downloadable content is Application Support directory, not Documents.

Application Support: Use this directory to store all app data files except those associated with the user’s documents. For example, you might use this directory to store app-created data files, configuration files, templates, or other fixed or modifiable resources that are managed by the app. An app might use this directory to store a modifiable copy of resources contained initially in the app’s bundle. A game might use this directory to store new levels purchased by the user and downloaded from a server.

(from File System Basics)

Don't have enough reps to add the comment so adding it as the answer.

The answer by Lësha Turkowski works for sure but was had issues with loading the particle images using only NSURL.

All particles were appearing square which meant,

If the value is nil (the default), SceneKit renders each particle as a small white square (colorized by the particleColor property).

SCNParticleSystem particleImage

In the documentation it says You may specify an image using an NSImage (in macOS) or UIImage (in iOS) instance, or an NSString or NSURL instance containing the path or URL to an image file.

Instead of using the NSURL, ended up using the UIImage and it loaded up fine.

    extension SCNParticleSystem {
    static func make(fromFileAt url: URL) -> SCNParticleSystem? {
        guard let data = try? Data(contentsOf: url),
            let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
            let system = object as? SCNParticleSystem else { return nil }

        if let particleImageName = system.particleImage as? String {
            let particleImageURL = url
                .deletingLastPathComponent()
                .appendingPathComponent(particleImageName)

            if FileManager.default.fileExists(atPath: particleImageURL.path) {
                // load up the NSURL contents in UIImage
                let particleUIImage = UIImage(contentsOfFile: particleImageURL.path)
                system.particleImage = particleUIImage
            }
        }

        return system
    }
}

I found out, that sometimes when dragging a SCNParticleSystem file into your project (probably form a different project) a silent error can happen due to some bugs in Xcode. As a result you can't get a reference to an instance of your SCNParticleSystem.

Solution: Check your BuildSettings in your target. The SCNPaticleSystem AND the associated ImageFile should be listed there and then you should get it right. (see screenShot below)

ScreenShot

Related