Path part of "filename.m4v" is "/private/tmp", which is unexpected - swift

I'm writing some Swift code to parse filenames of video files and extract the show, season and episode. These are returned as key/value parts in a dictionary. As part of my unit tests, I found something odd. First the code (comments and whitespace removed):
public static func parse(_ filename: String, defaults: [String: String] = [String: String]()) -> [String: String] {
let url = URL(fileURLWithPath: filename)
let file = url.deletingPathExtension().lastPathComponent
if file.count == 0 {
return ret
}
if file.count > 0 {
ret["file"] = file
}
let ext = url.pathExtension
if ext.count > 0 {
ret["extension"] = ext
}
let path = url.deletingLastPathComponent().path
if path.count > 0 {
ret["path"] = path
}
I called this in my test case thus...
ParseVideoFilename.parse("non-empty-filename.m4v")
And this is what resulted:
["ext": "m4v", "file": "non-empty-filename", "path": "/private/tmp"]
I am a bit surprised about that path. I did not pass that in, so I assume URL is doing something here. I don't expand the path nor resolve it. Is this expected behavior, and if so, why?

Running your code in a playground just gave me file:///private/var/folders/1p/wpwdypm96_s5zfwxxzvwwp0m0000gn/T/com.apple.dt.Xcode.pg/containers/com.apple.dt.playground.stub.iOS_Simulator.Playground-C18F0418-5C1D-4772-9AE9-E3EF9AA2F07C/non-empty-filename.m4v as the output of
let url = URL(fileURLWithPath: filename)
print(url.absoluteString)
This looks to me as the current directory of the process that gets executed by Playground. I'm not at all surprised by this, since we gave URL a relative path without any base and the file URI scheme doesn't really know how to handle relative paths (see e.g. this answer)
If you absolutely don't want to see these artifacts, you can modify your parser to use an absolute path:
let url = URL(fileURLWithPath: "/" + filename)
print(url.absoluteString) // prints "file:///non-empty-filename.m4v"

Related

How to get the url path of Apple ODR resource?

I am new in Xcode and Swift. Currently working on a script dealing Apple's ODR:
https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/On_Demand_Resources_Guide/Managing.html#//apple_ref/doc/uid/TP40015083-CH4-SW1
I am using NSBundleResourceRequest as
assetsPack = NSBundleResourceRequest(tags: [tag])
and than download a specific resource from my Assets.xcassets by a given tag with:
conditionallyBeginAccessingResources instance method.
The main code snippet I have is:
var assetsPack: NSBundleResourceRequest?
if let req = assetsPack{
req.assetsPack()
}
assetsPack = NSBundleResourceRequest(tags: [tag])
guard let req = assetsPack else {
return
}
req.conditionallyBeginAccessingResources{available in
if available{
print("available")
print(available)
self.anotherFunction(tag)
} else {
req.beginAccessingResources{error in
guard error == nil else{
return
}
self.anotherFunction(tag)
}
}
What I need is to return the path of the ODR resource here or pass it to another function. I need to be able and use this path to copy my file to another place, or access to it once its downloaded with another external plugin.
I've been trying some method like:
let path = req.bundle.url(forResource: "myFile", withExtension: "data")
print(path)
Considering that i have a myFile of type data in my Assets.xcassets.
But it returns nil.
I've tried also:
let stringPath = Bundle.main.path(forResource: "myFile", ofType: "data")
let urlPath = Bundle.main.url(forResource: "myFile", withExtension: "data")
It's is possible that you called endAccessingResources before you accessed the resource, or your request is released/nilified too early, which caused the system to deallocate the files.
An irrelevant side note: ODR tags don't seem to support whitespaces in them. If one adds a tag with whitespace, the request will execute without actually downloading the files. The behavior would be conditionallyBeginAccessingResources constantly giving isResourceAvailable = false.

FileHandle not accepting my URLs for write access

I'd like to open a uniquely named output file for writing either plist or data, but not having any luck in getting a handle using either URL routine of init(fileURLWithPath:) or init(string:)
func NewFileHandleForWritingFile(path: String, name: String, type: String, outFile: inout String?) -> FileHandle? {
let fm = FileManager.default
var file: String? = nil
var uniqueNum = 0
while true {
let tag = (uniqueNum > 0 ? String(format: "-%d", uniqueNum) : "")
let unique = String(format: "%#%#.%#", name, tag, type)
file = String(format: "%#/%#", path, unique)
if false == fm.fileExists(atPath: file!) { break }
// Try another tag.
uniqueNum += 1;
}
outFile = file!
do {
let fileURL = URL.init(fileURLWithPath: file!)
let fileHandle = try FileHandle.init(forWritingTo: fileURL)
print("\(file!) was opened for writing")
//set the file extension hidden attribute to YES
try fm.setAttributes([FileAttributeKey.extensionHidden: true], ofItemAtPath: file!)
return fileHandle
} catch let error {
NSApp.presentError(error)
return nil;
}
}
debugger shows
which for this URL init routine adds the scheme (file://) but otherwise the same as the other, and I'd like to prefer the newer methods which throw reutrning (-1) when just using paths. The error thrown (2) is an ENOENT (no such entity!?) as I need a handle to write to I'm confused how else to get one? The sample path is a new folder created at desktop to triage.
Unlike the previous answer, I recommend using Data's write(to:options:) API instead of FileManager's createFile(atPath:contents:attributes:), because it is a URL-based API, which is generally to be preferred over path-based ones. The Data method also throws an error instead of just returning false if it fails, so if something goes wrong, you can tell the user why.
try Data().write(to: fileURL, options: [])
I would also suggesting replacing the path-based FileManager.fileExists(atPath:) with the URL-based checkResourceIsReachable():
if false == ((try? fileURL.checkResourceIsReachable()) ?? false)
You can't create a file handle to a non-existent file. That is what is causing the ENOENT error.
Use FileManager createFile(atPath:contents:attributes:) to create the file just before creating the file handle.
do {
fm.createFile(atPath: file!, contents: nil, attributes: [FileAttributeKey.extensionHidden: true])
let fileURL = URL(fileURLWithPath: file!)
let fileHandle = try FileHandle(forWritingTo: fileURL)
print("\(file!) was opened for writing")
return fileHandle
} catch let error {
NSApp.presentError(error)
return nil;
}

Get path of a file in a data set located in Assets.xcassets

I have a data set of audio files in my Assets.xcassets:
I'm trying to get the path of one of those audio files like this:
let path: String = Bundle.main.path(forResource: "acoustic_grand_piano/A4", ofType: "f32")!
But I get a EXC_BAD_INSTRUCTION. I tried to look on the internet but I don't find anything on Data Sets.
How can I get the content of one of these files?
Thanks!
Try this:
Manually put your files into a folder, named anything you want.
Append ".bundle" to the folder to create a bundle. You'll get a warning, accept it. Congrats, you've just created your first bundle! :-)
Manually drag that folder into your app.
Get at your files by using the following code....
public func returnFile(_ named:String) -> String {
let path: String = Bundle.main.path(forResource: "myAudioFiles", ofType: "bundle")! + "/" + name + ".f32"
do {
return try String(contentsOfFile: path)
}
catch let error as NSError {
return error.description
}
}
Now, my files are text files of CIKernel code. Since your's are audio files you may need to change the String return to something else.
EDIT:
In my case I'm using a framework, as I wish to share these files/images with extensions and other apps. If you are working in such a set up, here's the unaltered code:
public func returnFile(_ resource:String, _ fileName:String, _ fileType:String) -> String {
let identifier = "com.companyname.appname" // replace with framework bundle identifier
let fileBundle = Bundle.init(identifier: identifier)
let filePath = (fileBundle?.path(forResource: resource, ofType: "bundle"))! + "/" + fileName + "." + fileType
do {
return try String(contentsOfFile: filePath)
}
catch let error as NSError {
return error.description
}
}

Swift: How to expand a tilde in a path String

How can I expand a path String with a tilde in Swift? I have a string like "~/Desktop" and I'd like to use this path with the NSFileManager methods, which requires the tilde to be expanded to "/Users/<myuser>/Desktop".
(This question with a clear problem statement doesn't exist yet, this should be easily findable. Some similar but not satisfying questions are Can not make path to the file in Swift, Simple way to read local file using Swift?, Tilde-based Paths in Objective-C)
Tilde expansion
Swift 1
"~/Desktop".stringByExpandingTildeInPath
Swift 2
NSString(string: "~/Desktop").stringByExpandingTildeInPath
Swift 3
NSString(string: "~/Desktop").expandingTildeInPath
Home Directory
Additionally you can get the home directory like this (returns a String/String?):
NSHomeDirectory()
NSHomeDirectoryForUser("<User>")
In Swift 3 and OS X 10.12 it's also possible to use this (returns a URL/URL?):
FileManager.default().homeDirectoryForCurrentUser
FileManager.default().homeDirectory(forUser: "<User>")
Edit: In Swift 3.1 this got changed to FileManager.default.homeDirectoryForCurrentUser
Return string:
func expandingTildeInPath(_ path: String) -> String {
return path.replacingOccurrences(of: "~", with: FileManager.default.homeDirectoryForCurrentUser.path)
}
Return URL:
func expandingTildeInPath(_ path: String) -> URL {
return URL(fileURLWithPath: path.replacingOccurrences(of: "~", with: FileManager.default.homeDirectoryForCurrentUser.path))
}
If OS less than 10.12, replace
FileManager.default.homeDirectoryForCurrentUser
with
URL(fileURLWithPath: NSHomeDirectory()
Here is a solution that does not depend on the NSString class and works with Swift 4:
func absURL ( _ path: String ) -> URL {
guard path != "~" else {
return FileManager.default.homeDirectoryForCurrentUser
}
guard path.hasPrefix("~/") else { return URL(fileURLWithPath: path) }
var relativePath = path
relativePath.removeFirst(2)
return URL(fileURLWithPath: relativePath,
relativeTo: FileManager.default.homeDirectoryForCurrentUser
)
}
func absPath ( _ path: String ) -> String {
return absURL(path).path
}
Test code:
print("Path: \(absPath("~"))")
print("Path: \(absPath("/tmp/text.txt"))")
print("Path: \(absPath("~/Documents/text.txt"))")
The reason for splitting the code into two methods is that nowadays you rather want URLs when working with files and folders and not string paths (all new APIs use URLs for paths).
By the way, if you just want to know the absolute path of ~/Desktop or ~/Documents and similar folders, there's an even easier way for that:
let desktop = FileManager.default.urls(
for: .desktopDirectory, in: .userDomainMask
)[0]
print("Desktop: \(desktop.path)")
let documents = FileManager.default.urls(
for: .documentDirectory, in: .userDomainMask
)[0]
print("Documents: \(documents.path)")
Swift 4 Extension
public extension String {
public var expandingTildeInPath: String {
return NSString(string: self).expandingTildeInPath
}
}

I cant read my text files from my application's Bundle

I used to read the text files from my application's bundle by using the following code. However, no matter what my application can't find them anymore. I am 100% sure that all my files are in the Assets.xcassets, I can see them, edit them, transform them from a directory to another. But my application doesn't want to read them, please tell me what I missed!!
this is the procedure I am using...
func readBundle(file:String) -> String
{
var res: String = ""
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "txt")
{
let fm = NSFileManager()
let exists = fm.fileExistsAtPath(path)
if(exists)
{
let c = fm.contentsAtPath(path)
res = NSString(data: c!, encoding: NSUTF8StringEncoding) as! String
}
}
return res
}
I am using it like this:
let res = readBundle("test")
print(res)
when storing non image files in XCAssets, you should use NSDataAsset to acccess their content
https://developer.apple.com/library/ios/documentation/UIKit/Reference/NSDataAsset_Class/
func readBundle(file:String) -> String
{
var res = ""
if let asset = NSDataAsset(name: file) ,
string = String(data:asset.data, encoding: NSUTF8StringEncoding){
res = string
}
return res
}
In the another option then 'XCAssets' you can create a separate folder/group of your resources other than images in the project structure, check if they exist in the Copy Bundle Resource in the Build phases section of your project's main target
If you add resource like this your current code should work as it is
func readBundle(file:String) -> String
{
var res: String = ""
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "txt")
{
//you should be able to get the path
//other code as you has written in the question
}
return res
}