Swift: How to unzip file not contained in my bundle? - swift

I am not very familiar with Swift programming but I need to write a small tool in Swift which can unzip a file (and then launch a program). I need to unzip a file which is not contained in my app bundle. It is located in /Users/me/folder1/folder2/openjdk-11.0.2.zip
I tried the libraries "Zip", "ZipFoundation", and "SSZipArchive". From what I read so far, I think that the libraries which I tried need the zip file to be located in the app bundle but I am not sure.
With "Zip" I tried:
_ = try Zip.quickUnzipFile(URL(string: openjdkZipUrl!.relativePath)!)
With "ZipFoundation" I tried:
let fileManager = FileManager()
let archive = openjdkZipUrl
let destinationURL = openjdkFolderUrl
do {
try fileManager.unzipItem(at: archive.url, to: destinationURL)
} catch {
}
ZipFoundation told me "Value of type 'FileManager' has no member 'unzipItem'" but I imported it with import Foundation. I also have it (and the other libraries) in my Podfile.
With "SSZipArchive" I tried:
let success = SSZipArchive.unzipFile(atPath: openjdkZipUrl!.path, toDestination: openjdkFolderUrl!.path)
The used paths are
let openjdkZip = "file:///Users/" + user + "/folder1/folder2/openjdk-11.0.2.zip"
let openjdkZipUrl = URL(string: openjdkZip)
and
let openjdkFolder = "file:///Users/" + user + "/folder1/folder2/openjdk-11.0.2"
let openjdkFolderUrl = URL(string: openjdkFolder)
Is it really a problem that the zip file is not contained in my bundle? Can someone tell me what I did wrong?
Thanks in advance

#trojanfoe mentioned in the comments:
Mac apps are sandboxed by default which means they have no access to a user's files unless they ask for it by getting the user to open the file/folder. You should ensure it's turned off for your app, however you are testing if the file exists and if this is succeeding then it looks like sandboxing is not turned on?
I looked at the .entitlements file in my project and found out that "App Sandbox" was set to "YES". I set it to "NO" and it worked perfectly.
It seems that you can check for a file while in sandbox mode (as I did with my condition) but not access them when trying to unzip them.
Thanks again #trojanfoe :)

Related

Trouble with Application Support directory

I searched for my problem with identifying location of the "Application support" on SO. However the various solutions I found are not working for me. Here's the code I'm using to locate a file I want to read/write from.
private var databaseUrl: URL?
{
let file = "MyApp/MyFile.txt"
if let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
{
let path = dir.appendingPathComponent(file)
return path
}
return nil
}
The Bundle Identifier of my app I have set to "MyApp" instead of "com.xxx.yyy.Myapp" which Xcode was using by default, and which I don't want. I was expecting to get a path like this...
/Users/griftopia/Library/Application Support/MyApp/MyFile.txt
This would be consistent with how I see other apps create files on my machine. However, what I am actually getting is this...
Users/griftopia/Library/Containers/MyApp-66665055012bb589084e4aeda209f212b5f695c7/Data/Library/Application Support/Myfile.txt)
Needless to say I want to say within the sandbox, but I thought Application Support is in the sandbox, but not expecting the path I got. Appreciate any insight

Read a file in a macOS Command Line Tool project

I've added a few JSON files to my macOS Command Line Tool project, but I can't seem to find them the usual way.
if let path = Bundle.main.path(forResource: "Data", ofType: "json")
{
if let contents = try? String(contentsOfFile: path)
{
print (contents)
}
else { print("Could not load contents of file") }
}
else { print("Could not find path") }
This code works absolutely fine when used in a View based application, but always prints "Could not find path" in the command line tool.
Does anyone know what I'm doing wrong?
P.S: Fully aware that I should be using guard statements for this, but the code isn't in a function, just faffing about in main.swift till I figure this out.
You can create a separate bundle, place the files there, and then load them without having to worry about their individual URLs.
Let's do it:
In Xcode, click on File --> New --> Target...
Click on the macOS tab at the top
Scroll down to the Framework & Library, select Bundle, click Next. Give a name to the new bundle (let's say JSONMocks).
Add files to this bundle. Add the file to Xcode, then make sure that the bundle is ticked in its target membership section:
Add the bundle to your target. In the target's Build Phases, open the Copy Files phase and click the + button. Select the bundle and click Add:
Programmatically access the contents of your bundle thus:
let currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
let bundleURL = URL(fileURLWithPath: "JSONMocks.bundle", relativeTo: currentDirectoryURL)
let bundle = Bundle(url: bundleURL)
let jsonFileURL = bundle!.url(forResource: jsonFileName, withExtension: "json")!
There is no app bundle for a command-line tool. Just a binary.
You need to put the JSON file in a known location (current directory?) and construct the path yourself

Xcode Project Folder Structure

I am using the latest XCode / Swift 3.0 and facing some problems:
I created a folder in /MyProject/MyProject/Assets/Subfolder1 and stored a few hundred .txt files in it to iterate over the files inside the Subfolder1 with the following code:
let fm = FileManager.default
fm.enumerator(atPath: "/MyProject/Assets/Subfolder1")?.forEach({ (e) in
if let e = e as? String, let url = URL(string: e) {
doSomethingWith(url.pathExtension)
}
})
But I cannot find any files. I tried some variations ("/Assets/Subfolder1/" and "/MyProject/MyProject/Assets/Subfolder1"), all are not working.
What is the correct path that I have to specify?
Best
Chris
First off, make sure you have added the actual folder to the Xcode project. You can do this simply by dragging and dropping the folder into your project. After you drag and drop, this window should appear
Make sure you select Create folder references and Copy items if needed. This will make sure that the entire folder is constantly synced inside your Xcode project. Then to verify that the assets will be transferred to your device when building the project, go into Build Phases and then to Copy Bundle Resources and you should see the name of the folder, in my case, I have named it TextFiles.
Now because you have added your folder into the application's Main Bundle, you can access it by doing this:
let fm = FileManager.default
let path = Bundle.main.resourcePath?.appending("/TextFiles")
fm.enumerator(atPath: path!)?.forEach({ (e) in
if let e = e as? String, let url = URL(string: e) {
doSomethingWith(url.pathExtension)
}
})
The reason we need to do the Bundle.main.resourcePath before it and can't simply say /TextFiles is because if we say /TextFiles it looks for that folder starting from the system root, which is why it wasn't working before. It would be like having a folder in C:/Users/Abc123/Documents/TextFiles but the system is looking for it in C:/TextFiles.

Accessing File saved in today extension from project

I am trying to access a file I saved from my today extension.In my today extenson I did this to save the file:
func startRecording() {
let audioFilename = getDocumentsDirectory().appendingPathComponent("recording.m4a")
let settings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 12000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
do {
audioRecorder = try AVAudioRecorder(url: audioFilename, settings: settings)
audioRecorder.delegate = self
audioRecorder.record()
recordButton.setTitle("Stop", for: .normal)
} catch {
finishRecording(success: false)
}
}
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
I then tried to get the data for my AVAudio Player in the main part of the project using this code:
let path = Bundle.main.path(forResource: "recording.m4a", ofType:nil)!
let url = URL(fileURLWithPath: path)
However, it gave the error: unexpectedly found nil while unwrapping an Optional value.
Thanks for the help.
Your extension saves the file to its document directory and your app code is looking for the file in the app bundle. The app bundle only contains the resources that are distributed with the app. You'll need to delve into the file system.
However, there's another problem. The extension and containing app don't share a documents directory. They each have their own container for writing data local to themselves. If you want to share data between them, it's a little more work. In summary:
Create an app group identifier for the app and the extension to share.
Use FileManager.containerURL(forSecurityApplicationGroupIdentifier:) to get the file URL for the shared container directory.
From the container URL, append the file name.
In the extension, you'll set up the AVAudioRecorder as usual and start recording.
In the main app, you'll want to use the NSFileCoordinator API to ensure that only one process is writing to the file at a time. Hopefully, the AVAudioRecorder uses the NSFileCoordinator API internally, although I didn't immediately find confirmation of this.
For more details about shared containers, see this blog post.
I just tried the same - record audio from a Today Extension. The code looks sooo familiar, so I'm taking a wild guess: you want to capture voice and send the file to the Google Speech API, correct?
Nonetheless, I think we're hitting restrictions of extensions: judging by https://developer.apple.com/library/content/qa/qa1872/_index.html extensions cannot record audio. The article has been writting for iOS 8, but I don't believe Apple ever lifted the restriction. Please correct me if I'm wrong, since I keep running into problems doing what OP does myself.
btw, check the result of audioRecorder.record(). It might be false and indicate that the capture never started (that's my error currently).

NSBundle.mainBundle().URLForResource("bach1", withExtension: "jpg") returning null

NSBundle.mainBundle().URLForResource("bach1", withExtension: "jpg")
The above code is returning null.
In order to check if the file exists or not, I used below code:
let fileManager = NSFileManager.defaultManager()
if fileManager.fileExistsAtPath(savepath) {
println("exist")
}
The above code returns that file exists in directory.
So I don't understand why the first code is returning null
Your problem is that NSBundle.mainBundle().URLForResource("bach1", withExtension: "jpg") returns an optional NSURL. You need to use if let to unwrap it and extract your file path from the returned url as follow:
if let resourceUrl = NSBundle.mainBundle().URLForResource("bach1", withExtension: "jpg") {
if NSFileManager.defaultManager().fileExistsAtPath(resourceUrl.path!) {
print("file found")
}
}
Swift 3.x
if let resourceUrl = Bundle.main.url(forResource: "bach1", withExtension: "jpg") {
if FileManager.default.fileExists(atPath: resourceUrl.path) {
print("file found")
}
}
I found that you have to actually go into Build Phases -> Copy Bundle Resources and manually add your item sometimes to the bundle. In my case it was a video file that didn't get added to the bundle correctly on its own.
NSBundle.URLForResource only returns files that are bundled in your application at build/compile time, like when you drag a file into your Xcode project and add it to your app target. You can typically use this when your app comes preloaded with resources you need, like images, movies, databases, and other files.
It seems like savepath is a path your application wrote to while running (maybe into your app's Documents directory?). Because of this, the file is outside of the bundle and you will need to store that path/URL somewhere (or build it up again) if you need to reference the file in the future.
I had this issue too. My fix was:
Selecting the file in project navigator
Open the 'File Inspector' (on the right hand side pane)
Checking the app target in the Target Membership