Swift 4 alternative if let - swift

I have this code, it checks if a file exists in the document directory, if it does it used this to set up the data for the controller, if it doesn't it uses a file supplier in the bundle, and if also that fails it set up an empty view controller.
Is there a way to conditionally set the url variable so that I can write the parse(fromFile:) method once instead of in an if else? The most intuitive way would appear to be if let url = something || let url = somethingElse then perform in the parse(fromFile: url) in the brackets, but it appears that it's not possible with swift.
if let url = try? FileManager().url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true).appendingPathComponent("database.json") {
parse(fromFile: url)
} else if let url = Bundle.main.url(forResource: "database", withExtension: "json") {
parse(fromFile: url)
} else {
setUpWithNothing()
}

I would recommend to split the code into several steps:
let documentsUrl = try! FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
The documents directory should always exists therefore I am not too worried about using try!. However, if you worry, you should solve that special case separately, e.g. using:
guard let documentUrl = try? ... else {
setUpWithNothing()
return
}
Then:
let fileUrl = documentUrl.appendingPathComponent("database.json")
This call never fails, it doesn't check the existence of the file. You need to check existence explicitly:
let fileExists = FileManager.default.fileExists(atPath: fileUrl.path)
let defaultUrl = Bundle.main.url(forResource: "database", withExtension: "json")
let databaseUrl = fileExists ? fileUrl : defaultUrl!
Again, you shouldn't worry about the existence of the file in your bundle, if you know it's there and simply call:
parse(fromFile: databaseUrl)
However, if you want to be extra careful, just remove the ! and use:
if let databaseUrl = fileExists ? fileUrl : defaultUrl {
parse(fromFile: databaseUrl)
} else {
setUpWithNothing()
}

You can use the nil-coalescing operator (??):
If the expression with FileManager returns nil (by throwing, thanks to the try?), then Bundle.main.url(...) will be used instead.
If that is also nil, then the conditional binding will fail entirely, and the else block will be run setUpWithNothing().
if let url = (try? FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
).appendingPathComponent("database.json"))
?? Bundle.main.url(forResource: "database", withExtension: "json") {
parse(fromFile: url)
} else {
setUpWithNothing()
}

Since you're doing the same thing in the first two cases and something different in the second, this is a good candidate for it's own function:
func databaseFileURL() -> URL? {
if let url = try? FileManager().url(for: .documentDirectory, in: .userDomainMask,
appropriateFor: nil, create: true) {
return url.appendingPathComponent("database.json")
}
else {
return Bundle.main.url(forResource: "database", withExtension: "json")
}
}
and then you can just do
if let url = databaseFileURL() {
parse(fromFile: url)
} else {
setUpWithNothing()
}
It nicely separates concerns and makes it cleaner and clearer at the point of use. And if you need to do anything else with the file you can just call the function rather than copy-pasting the logic.

Related

Returning contents of file as Data in Swift

For the first time, I am trying to return a data file from documents. I am getting an error: No exact matches in call to initializer
Here's my code to retrieve the data file:
func returnGeoJsonFromDocumentsDirectory(eventId: String) -> Data{
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
let filePath = paths.appendingPathComponent("\(eventId).json")
let geoJsonFile = Data(contentsOfFile: filePath)
if geoJsonFile != nil {
return geoJsonFile!
}
else {
print("Unable to return json")
let emptyData = Data()
return emptyData
}
If I use NSData, this seems to solve the issue but I was under the impression you shouldn't NSData in Swift.
You are mixing up NS... with native Swift and String with URL related APIs.
This is a pure Swift version preferring the URL related API
func returnGeoJsonFromDocumentsDirectory(eventId: String) -> Data {
let documentsFolderURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileURL = documentsFolderURL.appendingPathComponent(eventId).appendingPathExtension("json")
do {
return try Data(contentsOf: fileURL) {
} catch {
print("Unable to return json", error)
return Data()
}
}
Or hand over the error to the caller
func returnGeoJsonFromDocumentsDirectory(eventId: String) throws -> Data {
let documentsFolderURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileURL = documentsFolderURL.appendingPathComponent(eventId).appendingPathExtension("json")
return try Data(contentsOf: fileURL)
}
I figured it out. This is for anyone else who has issues with retrieving Data.
func returnGeoJsonFromDocumentsDirectory(eventId: String) -> Data{
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let url = URL(fileURLWithPath: path)
let pathComponent = url.appendingPathComponent("\(eventId).json")
do {
let geoJsonFile = try Data(contentsOf: pathComponent)
print("Succesfully retrieved json file")
return geoJsonFile
}
catch {
print("Failed to get json file = \(error.localizedDescription)")
}
let emptyData = Data()
return emptyData
}

Different path URL for FileManager everytime I open the app [duplicate]

This question already has answers here:
NSFileManager.defaultManager().fileExistsAtPath returns false instead of true
(2 answers)
Closed 8 months ago.
I want to create one folder in the fileManager root path, but before creating it, I want to check that the folder exist or not, and if not, I will create, otherwise will leave it
here are the function that I use
public func isDirectoryExist(path: String) -> Bool {
let fileManager = FileManager.default
var isDir : ObjCBool = false
if fileManager.fileExists(atPath: path, isDirectory:&isDir) {
if isDir.boolValue {
return true
} else {
return false
}
} else {
return false
}
}
public func createNewDirectory(name: String) {
let DocumentDirectory = NSURL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
let DirPath = DocumentDirectory.appendingPathComponent(name)
do
{
try FileManager.default.createDirectory(atPath: DirPath!.path, withIntermediateDirectories: true, attributes: nil)
}
catch let error as NSError
{
Logger.logError("Unable to create directory \(error.debugDescription)")
}
Logger.logInfo("Dir Path = \(DirPath!)")
}
Now, when I check the folder existing, it's always false and create a new folder and it happen every time
func createARObjectDirectory() {
let rootURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
if isDirectoryExist(path: "\(rootURL.absoluteString)\(DefaultURL.arObjectUrlDirectoryName)") {
Logger.logServer("ARObject directly found")
} else {
createNewDirectory(name: DefaultURL.arObjectUrlDirectoryName)
}
}
Then I print the root url, and seems the hash in the middle of url is always different, how I can check it?
file:///var/mobile/Containers/Data/Application/5AD0690B-498D-4309-8BD0-191FB88766AC/Documents/AR-Object/
file:///var/mobile/Containers/Data/Application/41D35A54-1807-417E-AE29-311D43FCC21D/Documents/AR-Object/
file:///var/mobile/Containers/Data/Application/F7E385CC-7921-4C37-B9BF-BCEFFC2AEE9E/Documents/AR-Object/
file:///var/mobile/Containers/Data/Application/4748B014-5E55-46BB-BC83-394A6BC27292/Documents/AR-Object/
Thanks for your help
You are using the wrong API. absoluteString (in rootURL.absoluteString) returns the string representation including the scheme file://. The correct API for file system URLs is path
I recommend to use the URL related API as much as possible
public func directoryExists(at url: URL) -> Bool {
let fileManager = FileManager.default
var isDir : ObjCBool = false
if fileManager.fileExists(atPath: url.path, isDirectory:&isDir) {
return isDir.boolValue
} else {
return false
}
}
and compose the URL in a more reliable way
func createARObjectDirectory() {
let rootURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
if directoryExists(at: rootURL.appendingPathComponent(DefaultURL.arObjectUrlDirectoryName) {
Logger.logServer("ARObject directly found")
} else {
createNewDirectory(name: DefaultURL.arObjectUrlDirectoryName)
}
}
And this is swiftier too
public func createNewDirectory(name: String) {
let documentDirectory = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let dirURL = documentDirectory.appendingPathComponent(name)
do
{
try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: false, attributes: nil)
}
catch let error as NSError
{
Logger.logError("Unable to create directory \(error.debugDescription)")
}
Logger.logInfo("Dir Path = \(dirPath.path)")
}
The actual path to your app's documents directory is subject to change. You should use relative paths from the documents directory, and not try to compare paths between runs.
(I believe the path changes each time you rebuild your app or reinstall it, but is {fairly} stable for app store builds.)
Are you saying that the directory you create inside the documents directory goes away between runs? That should not be true. The contents of the documents directory should persist.

Write a bundle as a file

I want to write a bundle I've dragged and dropped into my project folder into a temporary file.
Previously I used fileManager.copyItem but it deleted the original.
Original code
func copyPackagedBundleToDocuments(withDestinationName dest: String) {
if let bundlePath = Bundle.main.path(forResource: embeddedBundle, ofType: nil) {
let fileManager = FileManager.default
let destPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first!
// applicationSupportDirectory is not created by default in the sandbox, and therefore we need to make sure that it exists!
let urls = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask)
if let applicationSupportURL = urls.last {
do {
try fileManager.createDirectory(at: applicationSupportURL, withIntermediateDirectories: true, attributes: nil)
} catch {
print(error)
}
}
let fullDestPath = NSURL(fileURLWithPath: destPath + "/" + dest)
do {
try fileManager.copyItem(atPath: bundlePath, toPath: fullDestPath.path!)
} catch {
print(error)
}
}
}
This tells me the paths etc. are fine, since it does copy the file.
Now I want an atomic copy, so want to use Data(contentsOfFile)
So I rewrote everything:
func copyPackagedBundleToDocuments(withDestinationName dest: String) {
if let bundlePath = Bundle.main.path(forResource: embeddedBundle, ofType: nil) {
let fileManager = FileManager.default
let destPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first!
// applicationSupportDirectory is not created by default in the sandbox, and therefore we need to make sure that it exists!
let urls = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask)
if let applicationSupportURL = urls.last {
do {
try fileManager.createDirectory(at: applicationSupportURL, withIntermediateDirectories: true, attributes: nil)
} catch {
print(error)
}
}
let urlPath = URL(fileURLWithPath: destPath + "/" + dest)
let dta = try? Data(contentsOf: URL(fileURLWithPath: bundlePath) )
do {
try dta.write(to: urlPath , options: .atomic)
} catch {
print ("error \(error)")
}
}
}
However the data (dta) here is nil!
I think this is because my bundle location (as follows) ends in a backslash /:
file:///Users/user/Library/Developer/CoreSimulator/Devices/37A85B0B-F2B7-4A0C-BAA7-E05A831FFAE0/data/Containers/Bundle/Application/AD665103-E256-4F6C-8248-B62D8FF9FDC8/LocalBundle.app/Bundle.bundle/
and I guess that copyItem can treat the bundle as a single file, but writing the bundle to data is somehow not possible (the path is correct, since the code using copyItem works).
For clarity the following
if let bundleURL = Bundle.main.url(forResource: embeddedBundle, withExtension: nil) {
print ( try? Data(contentsOf: bundleURL ) )
}
prints nil to the console, validating the url for the bundle.
How can I write my bundle to a file, atomically?

create plist and copy it to folder MacOS Swift

I have been racking my brain on how to create and write a plist to a certain Folder Directory in MacOS. In my case to the LaunchDaemons folder in /Library. I know how to create the plist but its the writing to the LaunchDaemons folder that I am having issues with. This code below from my understanding is for the sandbox but how do I do it outside of the sandbox? Cheers
let fileManager = FileManager.default
let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let path = documentDirectory.appending("test.plist")
I have added the code with the help I have received and have no errors but it is not writing anything to the folder. Here is the code:
let libraryDirectory = try! FileManager.default.url(for: .libraryDirectory, in: .localDomainMask, appropriateFor: nil, create: false)
let launchDaemonsFolder = libraryDirectory.appendingPathComponent("LaunchDaemons/test.plist")
if FileManager.default.fileExists(atPath: launchDaemonsFolder.path) {
print(launchDaemonsFolder)
let plistDictionary : [String: Any] = [
"ExitTimeOut": 600,
"Label": "BOOT.SHUTDOWN.SERVICE",
"ProgramArguments": ["/test.sh"] as Array,
"RunAtLoad": false,
"WorkingDirectory": "/"
]
let dictionaryResult = NSDictionary(dictionary: plistDictionary)
let fileWritten = dictionaryResult.write(to: launchDaemonsFolder, atomically: true)
print("is the file created: \(fileWritten)")
} else {
print("File Exists")
}
First of all NSSearchPathForDirectoriesInDomains is outdated, it's highly recommended to use the URL related API anyway.
This code creates an URL pointing to /Library/LaunchDaemons
let libraryDirectory = try! FileManager.default.url(for: .libraryDirectory, in: .localDomainMask, appropriateFor: nil, create: false)
let launchDaemonsFolder = libraryDirectory.appendingPathComponent("LaunchDaemons")

Return file if exists, otherwise return false

I'm trying to make a String extension that searches for a file in my app's directory and either returns that file or returns false if it does not exist. Here's what I have:
extension String {
func doesFileWithNameExist() -> Bool {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let url = NSURL(fileURLWithPath: path)
let filePath = url.appendingPathComponent(self+".png")?.path
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath!) {
return true
} else {
return false
}
}
}
Right now my function just returns a Bool, but I'm wondering if there's a way to just return the file if it exists, otherwise return false. Is there a way to return different value types from a function?
It's highly recommended to use the URL related API. This returns an optional UIImage:
extension String {
func doesFileWithNameExist() -> UIImage? { // maybe better pngImageInDocumentsFolder()
let fileManager = FileManager.default
do {
let url = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fullURL = url.appendingPathComponent(self).appendingPathExtension("png")
_ = try fullURL.checkResourceIsReachable()
let data = try Data(contentsOf: fullURL)
return UIImage(data: data)
} catch {
return nil
}
}
}
or using Leo's great reduction (slightly still reduced):
var image: UIImage? {
guard let url = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(self).appendingPathExtension("‌​png"),
let data = try? Data(contentsOf: url) else { return nil }
return UIImage(data: data)
}
You can return an optional value here, which in Swift is "a thing or nil" – very close to "that file or false." For example, here's a quick tweak to your extension function that returns the path if a file exists there, or nil otherwise:
extension String {
func doesFileWithNameExist() -> String? {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let url = NSURL(fileURLWithPath: path)
let filePath = url.appendingPathComponent(self+".png")?.path
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath!) {
return filePath
} else {
return nil
}
}
}
A quick editorial comment, though: this kind of behavior doesn't seem especially well-suited to a String extension. I'd consider writing an extension on FileManager instead, passing in a string for the file's name. Given the hardcoding of the "png" extension, maybe something with the following signature?
extension FileManager {
func pathToExistingPNGFile(named name: String) -> String? {
// …
}
}