SWIFT writing to plist is not updating - swift

I'm trying to write to the plist and I'm using two approaches but none of them work for me.
I'm not getting any errors though and when I print the paths I can see that plist exist, however you can see from the screenshot that the plist it is not getting updated/populated.
let path = Bundle.main.path(forResource: "Employee", ofType: "plist")!
let data : NSDictionary =
["A": [["userid":"1","username":"AAA","usergroupid":"2"], ["userid":"33","username":"ABB","usergroupid":"8"]],
"B": [["userid":"2","username":"BBB","usergroupid":"8"], ["userid":"43","username":"ABC","usergroupid":"8"]] ]
//first approach
let favoritesDictionary = NSDictionary(object: data, forKey: ("Favorites" as NSString?)!)
print(path)
let succeeded = favoritesDictionary.write(toFile: path, atomically: true)
//second approach
let bundlePath = Bundle.main.path(forResource: "Employee", ofType: "plist")!
print(bundlePath)
let dictionary = NSMutableDictionary(contentsOfFile: bundlePath)
dictionary?.setObject(data, forKey: ("Locations" as NSString?)!)
dictionary?.write(toFile: bundlePath, atomically: true)
Can someone please help?

This is a short tutorial.
Create your plist file and put it in the application bundle.
In AppDelegate create a computed property to get the current Documents folder and append the file path
var employeePlistURL : URL {
let documentsFolderURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
return documentsFolderURL.appendingPathComponent("Employee.plist")
}
In AppDelegate applicationWillFinishLaunching register a key-value pair for the firstLaunch flag in UserDefaults and copy the plist into the documents folder if the flag is true
func applicationWillFinishLaunching(_ aNotification: Notification) {
let defaults = UserDefaults.standard
defaults.register(defaults: ["firstLaunch":true])
if defaults.bool(forKey: "firstLaunch") {
let sourceFile = Bundle.main.url(forResource: "Employee", withExtension: "plist")!
try? FileManager.default.copyItem(at: sourceFile, to: employeePlistURL)
defaults.set(false, forKey: "firstLaunch")
}
}
Wherever you need to read and write the property list create also the computed property and add a property for the dictionary
var employees = [String:Any]()
and two methods to load and save the data
func loadEmployees() {
do {
let data = try Data(contentsOf: employeePlistURL)
guard let plist = try PropertyListSerialization.propertyList(from: data, format: nil) as? [String:Any] else { return }
employees = plist
} catch { print(error) }
}
func saveEmployees() {
do {
let data = try PropertyListSerialization.data(fromPropertyList: employees, format: .binary, options: 0)
try data.write(to: employeePlistURL)
} catch { print(error) }
}
A better way is to use structs and PropertyListEncoder/-Decoder but as the literal dictionary and the screenshot in the question are rather different I provide the common Dictionary / PropertyListSerialization way.

Related

How do I solve the strange "filehandle" problem? [duplicate]

I know there are a few questions pertaining to this, but they're in Objective-C.
How can I access a .txt file included in my app using Swift on an actual iPhone? I want to be able to read and write from it. Here are my project files if you want to take a look. I'm happy to add details if necessary.
Simply by searching in the app bundle for the resource
var filePath = NSBundle.mainBundle().URLForResource("file", withExtension: "txt")
However you can't write to it because it is in the app resources directory and you have to create it in the document directory to write to it
var documentsDirectory: NSURL?
var fileURL: NSURL?
documentsDirectory = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).last!
fileURL = documentsDirectory!.URLByAppendingPathComponent("file.txt")
if (fileURL!.checkResourceIsReachableAndReturnError(nil)) {
print("file exist")
}else{
print("file doesnt exist")
NSData().writeToURL(fileURL!,atomically:true)
}
now you can access it from fileURL
EDIT - 28 August 2018
This is how to do it in Swift 4.2
var filePath = Bundle.main.url(forResource: "file", withExtension: "txt")
To create it in the document directory
if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last {
let fileURL = documentsDirectory.appendingPathComponent("file.txt")
do {
if try fileURL.checkResourceIsReachable() {
print("file exist")
} else {
print("file doesnt exist")
do {
try Data().write(to: fileURL)
} catch {
print("an error happened while creating the file")
}
}
} catch {
print("an error happened while checking for the file")
}
}
Swift 3, based on Karim’s answer.
Reading
You can read files included in an app’s bundle through the bundle’s resource:
let fileURL = Bundle.main.url(forResource:"filename", withExtension: "txt")
Writing
However, you can’t write there. You will need to create a copy, preferably in the Documents directory:
func makeWritableCopy(named destFileName: String, ofResourceFile originalFileName: String) throws -> URL {
// Get Documents directory in app bundle
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last else {
fatalError("No document directory found in application bundle.")
}
// Get URL for dest file (in Documents directory)
let writableFileURL = documentsDirectory.appendingPathComponent(destFileName)
// If dest file doesn’t exist yet
if (try? writableFileURL.checkResourceIsReachable()) == nil {
// Get original (unwritable) file’s URL
guard let originalFileURL = Bundle.main.url(forResource: originalFileName, withExtension: nil) else {
fatalError("Cannot find original file “\(originalFileName)” in application bundle’s resources.")
}
// Get original file’s contents
let originalContents = try Data(contentsOf: originalFileURL)
// Write original file’s contents to dest file
try originalContents.write(to: writableFileURL, options: .atomic)
print("Made a writable copy of file “\(originalFileName)” in “\(documentsDirectory)\\\(destFileName)”.")
} else { // Dest file already exists
// Print dest file contents
let contents = try String(contentsOf: writableFileURL, encoding: String.Encoding.utf8)
print("File “\(destFileName)” already exists in “\(documentsDirectory)”.\nContents:\n\(contents)")
}
// Return dest file URL
return writableFileURL
}
Example usage:
let stuffFileURL = try makeWritableCopy(named: "Stuff.txt", ofResourceFile: "Stuff.txt")
try "New contents".write(to: stuffFileURL, atomically: true, encoding: String.Encoding.utf8)
Just a quick update for using this code with Swift 4:
Bundle.main.url(forResource:"YourFile", withExtension: "FileExtension")
And the following has been updated to account for writing the file out:
var myData: Data!
func checkFile() {
if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last {
let fileURL = documentsDirectory.appendingPathComponent("YourFile.extension")
do {
let fileExists = try fileURL.checkResourceIsReachable()
if fileExists {
print("File exists")
} else {
print("File does not exist, create it")
writeFile(fileURL: fileURL)
}
} catch {
print(error.localizedDescription)
}
}
}
func writeFile(fileURL: URL) {
do {
try myData.write(to: fileURL)
} catch {
print(error.localizedDescription)
}
}
This particular example is not the most flexible, but with a little bit of work you can easily pass in your own file names, extensions and data values.
🎁 Property Wrapper - Fetch and convert to correct data type
This simple wrapper helps you to load any file from any bundle in a cleanest way:
#propertyWrapper struct BundleFile<DataType> {
let name: String
let type: String
let fileManager: FileManager = .default
let bundle: Bundle = .main
let decoder: (Data) -> DataType
var wrappedValue: DataType {
guard let path = bundle.path(forResource: name, ofType: type) else { fatalError("Resource not found: \(name).\(type)") }
guard let data = fileManager.contents(atPath: path) else { fatalError("Can not load file at: \(path)") }
return decoder(data)
}
}
Usage:
#BundleFile(name: "avatar", type: "jpg", decoder: { UIImage(data: $0)! } )
var avatar: UIImage
You can define any decoder to match your needs
Get File From Bundle in Swift 5.1
//For Video File
let stringPath = Bundle.main.path(forResource: "(Your video file name)", ofType: "mov")
let urlVideo = Bundle.main.url(forResource: "Your video file name", withExtension: "mov")
Bundles are read only. You can use NSBundle.mainBundle().pathForResource to access the file as read-only, but for read-write access you need to copy your document to Documents folder or tmp folder.
Bundles can be written. You can use Bundle.main.path to overwrite file by adding it into Copy Bundles Resource.
I have to use a file from another bundle. So, following code worked for me. Needful when you work with a frameworks.
let bundle = Bundle(for: ViewController.self)
let fileName = bundle.path(forResource: "fileName", ofType: "json")

Appending JSON data Swift

I am currently working on a school project where I need to be able to read, write and view information from a JSON file. For my assignment, I am attempting to build an app that is a dictionary where you can add your own words, definitions, etc.
I have been stuck on trying to write the new data to the JSON file without overwriting the old.
I also have an error on the last line that I am confused about.
Here is the code that I have so far.
func fileUrl() -> URL {
let documentURL = try!
FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
return documentURL.appendingPathComponent("data.json")
}
#IBAction func addWords(_ sender: UIButton) {
if let oldWords:[[String : String]] = getJsonData() as [[String:String]]? {
if let oldJson = try? JSONSerialization.data(withJSONObject: oldWords, options: []) {
// Add old words to JSON file
}
}
let data: [String:String] = [
"Name": nameField.text ?? "N/A",
"Part of Speech": posField.text ?? "N/A",
"Definition": defView.text ?? "N/A"
]
let url = fileUrl()
if let jsonData = try? JSONSerialization.data(withJSONObject: data, options: []) {
// Append data into JSON file
print(data)
nameField.text = ""
defView.text = ""
posField.text = ""
} else {
print("Failed to save")
}
}
func getJsonData() -> [[String:String]]? {
let url = fileUrl()
let responseData: Data? = try! Data(contentsOf: url)
if let responseData = responseData {
let json: String? = try? JSONSerialization.jsonObject(with: responseData, options: []) as? String
if let dictionary: [[String:String]]? = json as? [[String:String]]? {
return dictionary
}
}
} // Missing return in a function expected to return '[[String : String]]?' error
#IBAction func loadData(_ sender: UIButton) {
let url = fileUrl()
let responseData: Data? = try! Data(contentsOf: url)
if let responseData = responseData {
let json: Any? = try? JSONSerialization.jsonObject(with: responseData, options: [])
if let json = json {
let dictionary: [String: Any]? = json as? [String: Any]
if let dictionary = dictionary {
for names in dictionary {
let name: String = dictionary["Name"] as! String
let definition: String = dictionary["Definition"] as! String
let pos: String = dictionary["Part of Speech"] as! String
print(name, definition, pos)
textView.text = ("Name: \(name) (\(pos))\n Definition: \(definition)\n ")
}
}
}
}
}
I have been researching a way to add the JSON data but maybe I have been staring at the code so long that I am missing an easy fix.
There is no native way in swift, nor with a third party library to append json objects to a file.
What you need to do is, when you call getJsonData -- you save the whole contents of that JSON into a variable and append from there. I recommend looking at Daniel's answer for an easy extension for appending JSON together. Let me know if you need any more help!
Regarding your error on the last line - It is good practice to never force un-wrap variables. It's hard to tell given we can't see the JSON tree; but make sure you are accessing the correct spot. print(dictionary) your dictionary and try some debugging and/or create validation for your array.

Get the Contents of multiple files in directory

I need to get the contents from multiple plist files and bring them into a single dictionary which is then displayed in a tableView
Using this code I can manually get each path and the contents of the file but I need to be able to do this for all plist files in the directory not just these predefined ones.
func getFiles() {
let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let path = documentDirectory.appending("/MainFolder/File1.plist")
let path1 = documentDirectory.appending("/MainFolder/File2.plist")
let path2 = documentDirectory.appending("/MainFolder/File3.plist")
tableViewData = [NSDictionary(contentsOfFile: path) as! [String : String], NSDictionary(contentsOfFile: path1) as! [String : String], NSDictionary(contentsOfFile: path2) as! [String : String]]
print(tableViewData)
}
I the display tableViewData in my tableView which gives me each files contents on its own row.
I am guessing I probably need an array of file urls filtered by .plist and then some way to get the contents of each file into a [String : String] dictionary.
I am new to swift, any help or a better way to do this would be great
First of all don't use outdated and objective-c-ish NSSearchPathForDirectoriesInDomains in Swift. Use the modernFileManager API.
Second of all don't use objective-c-ish NSDictionary(contentsOf to read property list data. Use PropertyListSerialization.
The function throws that means it hands over all possible errors to the caller. It filters the URLs in the directory by the plist extension and uses the map function to get the dictionary for each URL.
func getFiles() throws {
let documentDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let subFolderURL = documentDirectory.appendingPathComponent("MainFolder")
let allFiles = try FileManager.default.contentsOfDirectory(at: subFolderURL, includingPropertiesForKeys: nil)
let properListFiles = allFiles.filter{$0.pathExtension == "plist"}
tableViewData = try properListFiles.compactMap { url -> [String:String]? in
let data = try Data(contentsOf: url)
return try PropertyListSerialization.propertyList(from: data, format: nil) as? [String:String]
}
print(tableViewData)
}
Be aware that in sandboxed apps the Documents folder is located in the application container.
You can read all the plists and iterate over them adding their contents either to 1 array, or to an already kept dictionary.
do {
let directoryContents = try FileManager.default.contentsOfDirectory(at: documentDirectory, includingPropertiesForKeys: nil, options: [])
let allPlistFiles = directoryContents.filter{ $0.pathExtension == "plist" }
var aPlistArray = []
for aPlist in allPlistFiles {
aPlistArray.append(NSDictionary(contentsOfFile: path) as! [String : String])
}
} catch {
print(error)
}

reading from .plist always returns nil

All of my attempts to read from a plist have resulted in a nil value returned, I've tried this in several ways on both Xcode 6 & Xcode beta 7. Also, there are quite a few similar questions on stack, I've tried many of them, but none of them resolve this issue.
I've added my words.plist by clicking on:
{my project} > targets > build phases > copy Bundle Resources
Then I tried several variations of the following code in my ViewController:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true) as NSArray
let documentsDirectory = paths[0] as! String
let path = documentsDirectory.stringByAppendingPathComponent("words.plist")
let fileManager = NSFileManager.defaultManager()
//check if file exists
if(!fileManager.fileExistsAtPath(path)) {
// If it doesn't, copy it from the default file in the Bundle
if let bundlePath = NSBundle.mainBundle().pathForResource("words", ofType: "plist") {
let resultDictionary = NSMutableDictionary(contentsOfFile: bundlePath)
println("Bundle words file is --> \(resultDictionary?.description)") // this is nil!!!
fileManager.copyItemAtPath(bundlePath, toPath: path, error: nil)
} else {
println("words not found. Please, make sure it is part of the bundle.")
}
} else {
println("words already exits at path.")
// use this to delete file from documents directory
//fileManager.removeItemAtPath(path, error: nil)
}
print("entering if-let")
if let pfr = NSBundle.mainBundle().pathForResource("words", ofType: "plist") {
print("\nin let\n")
print(pfr)
print("\nentering dict if-let\n")
if let dict = NSDictionary(contentsOfFile: path) as? Dictionary<String, AnyObject> {
// use swift dictionary as normal
print("\nin let\n")
print(dict)
}
}
}
Question
Why am I getting a nil value and whats the proper way to add a plist file and read from it?
update:
inside my if statement the following is nil:
let resultDictionary = NSMutableDictionary(contentsOfFile: bundlePath)
println("Bundle words file is --> \(resultDictionary?.description)") // this is nil!!!
To me, this would indicate that either Xcode doesn't know about my words.plist file, or that I'm pointing my bundlePath to the wrong location.
the issue:
As #Steven Fisher stated, in the comments. My .plist file was an Array and not an NSDictionary. So I just had to switch two lines from my code:
let resultDictionary = NSMutableDictionary(contentsOfFile: bundlePath)
to
let resultDictionary = NSMutableArray(contentsOfFile: bundlePath)
and also
if let dict = NSDictionary(contentsOfFile: path) { //...
to
if let dict = NSArray(contentsOfFile: path) { //..
final working code:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true) as NSArray
let documentsDirectory = paths[0] as! String
let path = documentsDirectory.stringByAppendingPathComponent("words.plist")
let fileManager = NSFileManager.defaultManager()
//check if file exists
if(!fileManager.fileExistsAtPath(path)) {
// If it doesn't, copy it from the default file in the Bundle
if let bundlePath = NSBundle.mainBundle().pathForResource("words", ofType: "plist") {
let resultDictionary = NSMutableArray(contentsOfFile: bundlePath)
println("Bundle words file is --> \(resultDictionary?.description)")
fileManager.copyItemAtPath(bundlePath, toPath: path, error: nil)
} else {
println("words not found. Please, make sure it is part of the bundle.")
}
} else {
println("words already exits at path.")
// use this to delete file from documents directory
//fileManager.removeItemAtPath(path, error: nil)
}
print("entering if-let")
if let pfr = NSBundle.mainBundle().pathForResource("words", ofType: "plist") {
print("\nin let\n")
print(pfr)
print("\nentering dict if-let\n")
if let dict = NSArray(contentsOfFile: pfr) {
// use swift dictionary as normal
print("\nin let\n")
print(dict)
}
}
}

Swift NSDirectoryEnumerator Generator [duplicate]

I'm quite new to programming a Swift and I'm trying to iterate through the files in a folder.
I took a look at the answer here and tried to translate it to Swift syntax, but didn't succeed.
let fileManager = NSFileManager.defaultManager()
let enumerator:NSDirectoryEnumerator = fileManager.enumeratorAtPath(folderPath)
for element in enumerator {
//do something
}
the error I get is:
Type 'NSDirectoryEnumerator' does not conform to protocol 'SequenceType'
My aim is to look at all the subfolders and files contained into the main folder and find all the files with a certain extension to then do something with them.
Use the nextObject() method of enumerator:
while let element = enumerator?.nextObject() as? String {
if element.hasSuffix("ext") { // checks the extension
}
}
Nowadays (early 2017) it's highly recommended to use the – more versatile – URL related API
let fileManager = FileManager.default
do {
let resourceKeys : [URLResourceKey] = [.creationDateKey, .isDirectoryKey]
let documentsURL = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let enumerator = FileManager.default.enumerator(at: documentsURL,
includingPropertiesForKeys: resourceKeys,
options: [.skipsHiddenFiles], errorHandler: { (url, error) -> Bool in
print("directoryEnumerator error at \(url): ", error)
return true
})!
for case let fileURL as URL in enumerator {
let resourceValues = try fileURL.resourceValues(forKeys: Set(resourceKeys))
print(fileURL.path, resourceValues.creationDate!, resourceValues.isDirectory!)
}
} catch {
print(error)
}
I couldn't get pNre's solution to work at all; the while loop just never received anything. However, I did come across this solution which works for me (in Xcode 6 beta 6, so perhaps things have changed since pNre posted the above answer?):
for url in enumerator!.allObjects {
print("\((url as! NSURL).path!)")
}
my two cents from previously anwers.. more swifty and with optionals:
let enumerator = FileManager.default.enumerator(atPath: folderPath)
while let element = enumerator?.nextObject() as? String {
print(element)
if let fType = enumerator?.fileAttributes?[FileAttributeKey.type] as? FileAttributeType{
switch fType{
case .typeRegular:
print("a file")
case .typeDirectory:
print("a dir")
}
}
}
returns all files in a directory + in subdirectories
import Foundation
let path = "<some path>"
let enumerator = FileManager.default.enumerator(atPath: path)
while let filename = enumerator?.nextObject() as? String {
print(filename)
}
Swift3 + absolute urls
extension FileManager {
func listFiles(path: String) -> [URL] {
let baseurl: URL = URL(fileURLWithPath: path)
var urls = [URL]()
enumerator(atPath: path)?.forEach({ (e) in
guard let s = e as? String else { return }
let relativeURL = URL(fileURLWithPath: s, relativeTo: baseurl)
let url = relativeURL.absoluteURL
urls.append(url)
})
return urls
}
}
Based on code from #user3441734
Swift 3
let fd = FileManager.default
fd.enumerator(atPath: "/Library/FileSystems")?.forEach({ (e) in
if let e = e as? String, let url = URL(string: e) {
print(url.pathExtension)
}
})
In case that you are getting the
'NSDirectoryEnumerator?' does not have a member named 'nextObject' error
the while loop should be:
while let element = enumerator?.nextObject() as? String {
// do things with element
}
It has something to do with optional chaining
SWIFT 3.0
Returns all files with extension in the Directory passed & its subdirectories
func extractAllFile(atPath path: String, withExtension fileExtension:String) -> [String] {
let pathURL = NSURL(fileURLWithPath: path, isDirectory: true)
var allFiles: [String] = []
let fileManager = FileManager.default
let pathString = path.replacingOccurrences(of: "file:", with: "")
if let enumerator = fileManager.enumerator(atPath: pathString) {
for file in enumerator {
if #available(iOS 9.0, *) {
if let path = NSURL(fileURLWithPath: file as! String, relativeTo: pathURL as URL).path, path.hasSuffix(".\(fileExtension)"){
let fileNameArray = (path as NSString).lastPathComponent.components(separatedBy: ".")
allFiles.append(fileNameArray.first!)
}
} else {
// Fallback on earlier versions
print("Not available, #available iOS 9.0 & above")
}
}
}
return allFiles
}
Updating for Swift 3:
let fileManager = FileManager() // let fileManager = NSFileManager.defaultManager()
let en=fileManager.enumerator(atPath: the_path) // let enumerator:NSDirectoryEnumerator = fileManager.enumeratorAtPath(folderPath)
while let element = en?.nextObject() as? String {
if element.hasSuffix("ext") {
// do something with the_path/*.ext ....
}
}
Adding to vadian's response -- the Apple docs mention that Path-based URLs are simpler in some ways, however file reference URLs have the advantage that the reference remains valid if the file is moved or renamed while your app is running.
From the documentation for "Accessing Files and Directories":
"Path-based URLs are easier to manipulate, easier to debug, and are generally preferred by classes such as NSFileManager. An advantage of file reference URLs is that they are less fragile than path-based URLs while your app is running. If the user moves a file in the Finder, any path-based URLs that refer to the file immediately become invalid and must be updated to the new path. However, as long as the file moved to another location on the same disk, its unique ID does not change and any file reference URLs remain valid."
https://developer.apple.com/library/content/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/AccessingFilesandDirectories/AccessingFilesandDirectories.html
If you want to categorically check whether an element is a file or a subdirectory:
let enumerator = FileManager.default.enumerator(atPath: contentsPath);
while let element = enumerator?.nextObject() as? String {
if(enumerator?.fileAttributes?[FileAttributeKey.type] as! FileAttributeType == FileAttributeType.typeRegular){
//this is a file
}
else if(enumerator?.fileAttributes?[FileAttributeKey.type] as! FileAttributeType == FileAttributeType.typeDirectory){
//this is a sub-directory
}
}
Recently struggled with this when handling an array of urls, whether they be a directory or not (eg. drag and drop). Ended up with this extension in swift 4, may be of use
extension Sequence where Iterator.Element == URL {
var handleDir: [URL] {
var files: [URL] = []
self.forEach { u in
guard u.hasDirectoryPath else { return files.append(u.resolvingSymlinksInPath()) }
guard let dir = FileManager.default.enumerator(at: u.resolvingSymlinksInPath(), includingPropertiesForKeys: nil) else { return }
for case let url as URL in dir {
files.append(url.resolvingSymlinksInPath())
}
}
return files
}
}
Avoid reference URLs, while they do have some advantages as stated above, they eat system resources and if you’re enumerating a large filesystem (not that large actually) your app will hit a system wall quickly and get shutdown by macOS.