Swift 4.2 - Cocoapods: Adding and accessing scnassets in resource_bundles - ios

Attempting to create a cocoapod that utilizes an .scnassets folder. The folder is imported into Resources without any problems with the children objects, but I can't find a way to load the .dae files into an SCNScene. From my understanding there may be a problem with how XCode converts .dae into .scn assets.
.podspec includes:
s.resource_bundles = {
'ARUtilsScenes' => ['ARUtils/Assets/ARScenes.scnassets']
}
which properly loads the ARScenes.scnassets into Resources folder. I'm attempting to load a scene inside of my Pod (inside the pod and not a project that uses the pod, and have tried a variety of methods):
let arrowScene = SCNScene(named: "arrow.dae")
let arrowScene = SCNScene(named: "Resources/ARScenes.scnassets/arrow.dae")
let arrowScene = SCNScene(named: "arrow.dae", inDirectory: "Resources/ARScenes.scnassets", options: nil)
I've tried a variety of file/folder names, and am able to load images but I'm not able to get a .dae loaded as an SCNScene. Do I need to add .xib files to my resource_bundles, or is there some other way to ensure .scnassets folder properly compiles these .dae to .scn as well as make them available for loading into an SCNScene?

Suppose ZARKit is your framework distributed in CocoaPods.
.podspec includes:
s.resource_bundles = {
'ARUtilsScenes' => ['ARUtils/Assets/ARScenes.scnassets']
}
then you should do this
let bundle = Bundle(for: #Your Class#.self).
let resourcePath = bundle.path(forResource: <#T##String?#>, ofType: <#T##String?#>)
let resourceBundle = Bundle(path: resourcePath)
let url = resourceBundle.url(forResource: "ARScenes.scnassets/<#YourResourceName-arrow#>", withExtension: "<#scn#>")
let arrowScene = SCNScene(url: url)
edit: by Z. Bagley
Accepted answer, but this was the actual code I used:
// find bundle based on existing class in pod
let frameworkBundle = Bundle(for: ARUtilsClass.self) //ARUtilsClass is an empty class, Pod is only extensions for existing classes
// append the scenes bundle, scnassets, and scn/dae
let arrowURL = frameworkBundle.resourceURL?.appendingPathComponent("ARUtilsScenes.bundle/ARScenes.scnassets/newArrow.scn")
// create a node in parent
var arrow = SCNNode()
do {
// get arrow scene from bundle
let bundleArrowScene = try SCNScene(url: (arrowURL)!, options: nil)
// add arrow node from scene
arrow = bundleArrowScene.rootNode.childNode(withName: "arrow", recursively: true)!
} catch {
print("failed to find arrow resource")
}

When accessing the .dae asset from the .scnassets folder, there should be no need to include the "Resources/" in the path
For example: let arrowScene = SCNScene(named: "ARScenes.scnassets/arrow.dae")
Assuming that your .scnassets folder is called that.

There is a known bug introduced with updated build system by Apple which causes asynchronous compilation of asset catalogs making them unaccessible in the final build. For reference see this thread on Cocoapods GitHub. There are several workarounds for this listed, but none of them guarantee the results, so for now we seem to have to live with it

Related

FileManager can't locate PDF directory in Xcode project

I am trying to list all PDFs inside a folder that is stored in my Xcode project using SwiftUI, but after trying many different methods I found on here I cannot get it working.
Currently I am using file manager to list the items but when I try and print or list the items found it returns nil.
I have added the PDFs by dragging them into the Xcode project. I have also made sure that they are in my Copy Bundle Resource so I can use FileManager. See below:
Here is my Xcode structure, I am trying to list all items inside PDF. As you can see below the PDFs are stored outside the main folder structure in "/Swift/Products/Detail/Tab5/PDF", so when trying to list the files using Bundle.main.bundlePath, it looks at products.app.
Here is the code where I am trying to use FileManager to find all PDFs inside the folder:
struct ProductTab5View: View {
#State var files = getFiles()
var body: some View {
VStack{
ForEach(0..<files.count, id: \.self) { item in
Text(files[item])
}
}
}
}
func getFiles() -> Array<String>{
// Get document directory url
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
var files: [String] = []
do {
// Get the directory contents urls (including subfolders urls)
let directoryContents = try FileManager.default.contentsOfDirectory(at: documentsUrl, includingPropertiesForKeys: nil)
print(directoryContents)
// filter directory contents:
let pdfFiles = directoryContents.filter{ $0.pathExtension == "pdf" }
let pdfFileNames = pdfFiles.map{ $0.deletingPathExtension().lastPathComponent }
files.append(contentsOf: pdfFileNames)
} catch {
print(error)
}
return files
}
In such a way resources are copied flat into root application bundle, not user 's Documents as it was tried in provided code snapshot (and no PDF sub-directory is created for you).
So the solution would be just to iterate internal pdf files, like
func getFiles() -> Array<String> {
return Bundle.main.urls(forResourcesWithExtension: "pdf", subdirectory: nil)?
.compactMap { $0.lastPathComponent } ?? []
}
Tested with Xcode 12.1 / iOS 14.1
There are a few potential problems with your current code.
The first is this line:
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
Unless you have gone through a process separately from what you've shown of adding all of these files to the app's document directory, then there's no reason to believe that these files will be there. In other words: your Xcode project is not the same as your app's document directory. Think of the app's document directory as a place where you may store user-created or perhaps downloaded content as managed by the app -- not Xcode itself.
Next thing to check is whether all of these files are truly added to your target. Check your target's "Build Phases" -> "Copy Bundle Resources" section to see if they appear there.
If they do, you can use FileManager, but you have to access the correct directory, which is inside the main bundle -- not the app's user document directory.
The following answer goes into details about this (including making sure you create folder references): Getting a list of files in the Resources folder - iOS
The gist will be doing something like this:
if let files = try? FileManager.default.contentsOfDirectory(atPath: Bundle.main.bundlePath) { //may need to dig into subdirectories
for file in files {
//manipulate the file
}
}
and then using FileManager to list the documents in that path.
Note that you can also get all the files (including subdirectories) recursively by doing something like this:
let url = URL(fileURLWithPath: Bundle.main.bundlePath)
if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
for case let fileURL as URL in enumerator {
print(fileURL)
}
}
By using the above code during your debugging process, this should give you an insight into what your directory structure is really like inside your bundle. Once you've figured that out, adjust the previous code sample to be something like:
if let files = try? FileManager.default.contentsOfDirectory(atPath: Bundle.main.bundlePath + mySubPath) {
for file in files {
//manipulate the file
}
}
which will give you just the files in the one subdirectory you want once you fill in mySubPath. Note that if you want recursive search, you can use the code sample above.
You may want to exclude non-file items eventually -- see this answer for more details about recursive directory lists in Swift: listing all files in a folder recursively with swift

iOS: How to add Assets folder inside a Framework and how to access them in code?

I create an iOS app and added a framework to it. The generated framework doesn't have an assets folder like the generate Single View App. So I made an Assets folder inside the framework folder and drag and drop it to xcode, choose the target as my framework.
I tried using the asset but the asset doesn't show up. Can show one show me how to correctly do this? is it possible to create an assets folder inside a framework?
I am very new to iOS so any guidance would be much appreciated. Thanks in advance.
Add Asset catalog to framework target as usual via New File... > Resources, but to get resource from such asset it needs to specify bundle explicitly, as in below example...
Assuming that ImageProvider is in framework target, the code could be
public class ImageProvider {
// convenient for specific image
public static func picture() -> UIImage {
return UIImage(named: "picture", in: Bundle(for: self), with: nil) ?? UIImage()
}
// for any image located in bundle where this class has built
public static func image(named: String) -> UIImage? {
return UIImage(named: named, in: Bundle(for: self), with: nil)
}
}
of course you can name such class anyhow.
Here's how I do it.
First of all, here's how you can create a Bundle to hold your assets like images.
First, create a new target:
Navigate to main Xcode menu, File => New => Target. Choose the "macOS tab" then
from "Framework & Library" select "Bundle".
Give it your desired name and hit Finish. You should see the bundle in your project folder.
Second, Configuration changes in build settings of Bundle:
Go to Build Settings on your bundle target and change the Base SDK to be iOS.
Third, Add images:
Add your images to the Bundle directly, no need to add an assets folder. Just drag and drop.
Fourth, build the bundle:
Choose your bundle as a destination and choose the generic iOS device and hit Command + B
Fifth, the .bundle will appear in your products folder under your project folder. Right-click on it and view it in Finder and then drag and drop it inside of your main project folder.
Finally, here's how I'd access the assets inside of your bundle.
// Empty UIImage array to store the images in it.
var images = [UIImage]()
let fileManager = FileManager.default
let bundleURL = Bundle.main.bundleURL
let assetURL = bundleURL.appendingPathComponent("MyBundle.bundle") // Bundle URL
do {
let contents = try fileManager.contentsOfDirectory(at: assetURL,
includingPropertiesForKeys: [URLResourceKey.nameKey, URLResourceKey.isDirectoryKey],
options: .skipsHiddenFiles)
for item in contents { // item is the URL of everything in MyBundle imgs or otherwise.
let image = UIImage(contentsOfFile: item.path) // Initializing an image
images.append(image!) // Adding the image to the icons array
}
}
catch let error as NSError {
print(error)
}
You will have the .plist file inside of your bundle, therefore, I suggest you handle this by a simple condition to check if the file name is Info.plist don't create an image out of it.
Here's how I handled it in a very trivial way.
let fileManager = FileManager.default
let bundleURL = Bundle.main.bundleURL
let assetURL = bundleURL.appendingPathComponent("Glyphs.bundle")
do {
let contents = try fileManager.contentsOfDirectory(at: assetURL, includingPropertiesForKeys: [URLResourceKey.nameKey, URLResourceKey.isDirectoryKey], options: .skipsHiddenFiles)
for item in contents {
let imageName = item.lastPathComponent
if imageName != "Info.plist" {
let image = UIImage(contentsOfFile: item.path)
icons.append(image!)
}
}
}
catch {
//print(error)
showAlert(withTitle: "Error", message: "Can't get the icons.")
}
I had the same problem.
scenario:
our framework will be consumed iOS/OSX
a lot of PNGs inside Framework
we want pngs in iOSApp && MacOs
let's assume out framework project and target is "MyCustomFramework.framework" (usual yellow icon..) already in our app
-
func filePathInFrameworkBundle(name: String)->String{
let myBundlePath = Bundle.main.bundlePath
#if os(iOS)
let inMyFW = myBundlePath +
"/Frameworks" +
"/MyCustomFramework.framework"
#elseif os(OSX)
let inMyFW = myBundlePath +
"/Contents/Frameworks" +
"/MyCustomFramework.framework" + "/Versions/A/Resources"
#else
#endif
let pathInBundle = inMyFW+"/"+name
return pathInBundle
}
Now You can use THIS path in usual image loading calls.
For SwiftUI use:
Add an Asset catalog to your framework project (eg, "Media.xcassets").
Add a resource, like a Color Set. Let's say you name your color "Thunder".
Create a file in your framework called "Color+Extensions.swift" (or whatever you like).
Inside this file do:
import SwiftUI
private class LocalColor {
// only to provide a Bundle reference
}
public extension Color {
static var thunder: Color {
Color("Thunder", bundle: Bundle(for: LocalColor.self))
}
}
I imagine this works for other asset types as well.
In the project which is using your framework, once you have added the import for your framework, you should be able to just use the color normally:
Rectangle().foregroundColor(.thunder)

SCNParticleSystem load from document Directory

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.
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)

Path to pdf in directory with pdfKit

I am starting to use PDFKit, with a pdf file located in the root, it works with the following code:
if let path = Bundle.main.path(forResource: "mypdf1", ofType: "pdf") {
let url = URL(fileURLWithPath: path)
if let pdfDocument = PDFDocument(url: url) {
pdfView.autoScales = true
pdfView.displayMode = .singlePageContinuous
pdfView.displayDirection = .vertical
pdfView.document = pdfDocument
print(path)
}
}
But if I change the pdf file inside a directory for example "mydirectory", it does not work, my code is the following:
if let path = Bundle.main.path(forResource: "mypdf1", ofType: "pdf", inDirectory: "mydirectory") {
let url = URL(fileURLWithPath: path)
if let pdfDocument = PDFDocument(url: url) {
pdfView.autoScales = true
pdfView.displayMode = .singlePageContinuous
pdfView.displayDirection = .vertical
pdfView.document = pdfDocument
}
}
Any suggestions to fix the path problem.
UPDATE
According to the suggestion, try the following code, but I can not visualize the PDF either.
if let documentURL = Bundle.main.url(forResource: "mypdf1", withExtension: "pdf", subdirectory: "mydirectory") {
if let document = PDFDocument(url: documentURL) {
pdfView.autoScales = true
pdfView.backgroundColor = UIColor.lightGray
pdfView.document = document
}
}
Your code to read the pdf from a subdirectory is correct and as I read they get a nil in the path, that's for sure because your container folder has not been created correctly. You can see yellow and blue folder, your container folder should be blue.
To make blue folder you should do following steps:
Prepare folder structure with files in it.
Drag that folder into xcode i.e. project navigation pan.
Select " Create folder references for any added folders " option.
Finally click to add.
You will get that folder with blue color and your code can read the pdf.
There are two types of folders in Xcode: groups and folder references.
You can use groups to organize files in your project without affecting
their structure on the actual file system. This is great for code,
because you’re only going to be working with your code in Xcode. On
the other hand, groups aren’t very good for resource files.
On any reasonably complicated project, you’ll usually be dealing with
dozens – if not hundreds – of asset files, and those assets will need
to be modified and manipulated from outside of Xcode, either by you or
a designer. Putting all of your resource files in one flat folder is a
recipe for disaster. This is where folder references come in. They
allow you to organize your files into folders on your file system and
keep that same folder structure in Xcode.
Blue is used to represent a "Folder Reference".

Why are the audio files I've added to my xcode project available in the simulator but are not available when i deploy the app on iPhone

I am using an AVQueuePlayer to play local audio files which I have added to my Xcode project by dragging and dropping the .mp3 files into my folder in the project navigator pane. I then use this code to search thru my files and extract the files that I want, which I then use to fill a table view and to create AVPlayerItems for to play in my AVQueuePlayer.
My code works fine when I run the app on simulator but when i run the app on my iPhone, an error occurs and returns
fatal error: unexpectedly found nil while unwrapping an Optional value
Here is the code causing the issue...
var songNameArray: Array<String> = [String]()
let fileManager = NSFileManager.defaultManager()
let enumerator:NSDirectoryEnumerator = fileManager.enumeratorAtPath("/Users/UsersName/Desktop/Xcode Projects Folder/LocalAudioFilePlayer/LocalAudioFilePlayer")!
while let element = enumerator.nextObject() as? String {
if element.hasSuffix("mp3") {
songNameArray.append(element)
print(element)
}
}
Are the files not being properly copied into the bundle which is then deployed to the iPhone? If so, what is the proper way to add the audio files?
I also tried this...
var songNameArray: Array<String> = [String]()
let path = String(NSBundle.mainBundle().resourcePath)
let songFiles = try! NSFileManager.defaultManager().contentsOfDirectoryAtPath(path)
for item in songFiles {
if item.hasSuffix("mp3"){
songNameArray.append(item)
print("appended to songNameArray \(item)")
}
}
But I get the error
The folder “LocalAudioFilePlayer.app")” doesn’t exist
Any help is greatly appreciated
let path = String(NSBundle.mainBundle().resourcePath)
This line is not doing what you might think it does. NSBundle.mainBundle().resourcePath returns String?, which is an optional type. When you create a string from that by wrapping String() around it, it doesn't unwrap the optional, it creates a string describing the optional, which looks like this:
Optional("/Users/markbes/Library/Developer/CoreSimulator/Devices/18D62628-5F8A-4277-9045-C6DE740078CA/data/Containers/Bundle/Application/3DDC064C-0DD1-4BE9-8EA4-C151B65ED1E1/Resources.app")
What you want there is something more like
var path = NSBundle.mainBundle().resourcePath!
That unwraps the String? type, and gives you a string (or throws an error).
Did you select "Copy files if needed" then dragged mp3s into Project?
Try to load files as following:
let path = NSBundle.mainBundle().pathForResource("filename", ofType: "ext")
let url = NSURL.fileURLWithPath(path!)
var audioPlayer: AVAudioPlayer?
do {
try audioPlayer = AVAudioPlayer(contentsOfURL: url)
} catch {
print("Unable to load file")
}
Note: use only filename here, not the whole path.
It sounds like you haven't added your sound file to your target in the Xcode project. If they don't "belong" to a target, they won't get copied into the "resources" folder during building.
Select the sound files you want to use in the project navigator, and check the file inspector (view->Utilities->File Inspector, and make sure that the appropriate target is selected under "target membership".
edit: this is definitely not the problem in this case. Leaving this answer, as it may be useful for others running into a similar issue.

Resources