Archiving and unarchiving Arrays in the DocumentDirectory in Swift - arrays

I am trying to save a simple array of objects in the persistent memory by executing the following code:
let fileManager=NSFileManager()
let urls = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
if urls.count>0{
let localDocumentsDirectory=urls[0]
let archivePath=localDocumentsDirectory.URLByAppendingPathExtension("meditations.archive")
NSKeyedArchiver.archiveRootObject(self.meditationsArray, toFile: archivePath.path!)
let restored=NSKeyedUnarchiver.unarchiveObjectWithFile(archivePath.path!)
print("restored \(restored)")
}
}
Yet, when I print the restored date as in the code I find nil.
Conversely, if I use the CachesDirectory the array is soon after restored fine,
but when I reopen the app and try to load the data, it is lost. What is correct way to persistently save data?

I think the problem is that your are using URLByAppendingPathExtension, when you should be using URLByAppendingPathComponent. The "path extension" is the file extension, so your archivePath is "~/Documents.meditations.archive". It might be temporarily working with the CachesDirectory, because it's putting the data into a temporary file somewhere, or maybe just reading it back from memory. This should fix it:
let fileManager = NSFileManager()
let documentDirectoryUrls = fileManager.URLsForDirectory(.DocumentDirectory, .UserDomainMask)
if let documentDirectoryUrl = documentDirectoryUrls.first {
let fileUrl = documentDirectoryUrl.URLByAppendingPathComponent("meditations.archive")
// Also, take advantage of archiveRootObject's return value to check if
// the file was saved successfully, and safely unwrap the `path` property
// of the URL. That will help you catch any errors.
if let path = fileUrl.path {
let success = NSKeyedArchiver.archiveRootObject(meditationArray, toFile: path)
if !success {
print("Unable to save array to \(path)")
}
} else {
print("Invalid path")
}
} else {
print("Unable to find DocumentDirectory for the specified domain mask.")
}

I faced the same issue, I was unable to archive and unarchive array of objects using NSKeyedArchiver, I think the issue is that I'm using the below method :
NSKeyedArchiver.archiveRootObject(arrayOfItems, toFile: FileManager.getFileURL("My-File-Name")!)
I think this method is for archiving Objects, not array of Objects.
Anyway, I found a solution to my problem, by wrapping the whole array in an object, check below :
let myArrayItemsContainer = ArrayItemsContainer()
myArrayItemsContainer.allItems = arrayOfItems
NSKeyedArchiver.archiveRootObject(myArrayItemsContainer, toFile: FileManager.getFileURL("My-File-Name")!)
and I used the below code to unarchive my object :
NSKeyedUnarchiver.unarchiveObject(withFile: FileManager.getFileURL("My-File-Name")!) as? ArrayItemsContainer
Also I used this extension for using FileManager.getFileURL
public extension FileManager {
/// Returns the URL of the file given a name
///
/// - Parameter fileName: The file name of the file + extension
/// - Returns: The URL as String
static func getFileURL(_ fileName: String) -> String? {
let fileURL = FileManager().urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first
return (fileURL?.appendingPathComponent(fileName).path)
}
}

Related

How do I save multiple files in a package folder from a SwiftUI Document Based App?

I'm developing a SwiftUI document-based app that contains some easily serializable data plus multiple images. I'd like to save the document as a package (i.e, a folder) with one file containing the easily serialized data and a subfolder containing the images as separate files. My package directory should look something like this:
<UserChosenName.pspkg>/. // directory package containing my document data and images
PhraseSet.dat // regular file with serialized data from snapshot
Images/ // subdirectory for images (populated directly from my app as needed)
Image0.png
Image1.png
....
I've created a FileWrapper subclass that sets up the directory structure and adds the serialized snapshot appropriately but when I run the app in an iOS simulator and click on "+" to create a new document the app runs through the PkgFileWrapper init() and write() without error but returns to the browser window without apparently creating anything. I have declared that the Exported and Imported Type Identifiers conform to "com.apple.package". Can anyone suggest a way to get this working?
The PkgFileWrapper class looks like this:
class PkgFileWrapper: FileWrapper {
var snapshot: Data
init(withSnapshot: Data) {
self.snapshot = withSnapshot
let sWrapper = FileWrapper(regularFileWithContents: snapshot)
let dWrapper = FileWrapper(directoryWithFileWrappers: [:])
super.init(directoryWithFileWrappers: ["PhraseSet.dat" : sWrapper,
"Images" : dWrapper ])
// NOTE: Writing of images is done outside
// of the ReferenceFileDocument functionality.
}
override func write(to: URL,
options: FileWrapper.WritingOptions,
originalContentsURL: URL?) throws {
try super.write(to: to, options: options,
originalContentsURL: originalContentsURL)
}
required init?(coder inCoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The solution is to not override PkgFileWrapper.write(...). If the directory structure is set up correctly in the init(...) then the files and directories will be created automatically. The overridden write(...) function above has now been corrected.
If you want to write an image to the Images subdirectory, you could do something like the following:
func addImage(image: UIImage, name: String) {
let imageData = image.pngData()!
imageDirWrapper.addRegularFile(withContents: imageData,
preferredFilename: name)
}
The value of imageDirWrapper is the directory wrapper corresponding to the directory that holds your images, as created in PkgFileWrapper.init() above. A key concept you need to keep in mind here is that the "write" function will get called automatically at the appropriate time - you don't explicitly write out your image data. The ReferenceFileDocument class will arrange for that and will also arrange for your app to be passed the appropriate URL for setting up your file wrappers.
The imageDirWrapper variable is set in the required init(...) for the ReferenceFileDocument protocol:
required init(configuration: ReadConfiguration) throws {
phraseSet = PhraseSet()
if configuration.file.isDirectory {
if let subdir = configuration.file.fileWrappers {
// first load in the phraseSet
for (name, wrapper) in subdir {
if name == PkgFileWrapper.phraseSetFileName {
if let data = wrapper.regularFileContents {
phraseSet = try PhraseSet(json: data)
}
}
}
// next load in the images and put them into the phrases.
for (name, wrapper) in subdir {
if name == PkgFileWrapper.imageDirectoryName {
if let imageDir = wrapper.fileWrappers {
imageDirWrapper = wrapper
for (iName, iWrapper) in imageDir {
print("image file: \(iName)")
if let d = iWrapper.regularFileContents {
for p in phraseSet.phrases {
if p.imageName == iName {
// TBD: downsample
var uiD = ImageData(data: d)
if doDownSample {
uiD.uiimageData = downsample(data: d,
to: imageSize)
} else {
_ = uiD.getUIImage()
}
images[iName] = uiD
}
}
}
}
}
}
}
}
} else {
throw CocoaError(.fileReadCorruptFile)
}
You can see here how imageDirWrapper is set by looking through the passed-in directory's subdirectories for the image directory name. Also some bonus code: it first looks through the passed-in directory for the data file and loads it in; then it looks for the image directory and processes it.

Cannot assign value of type [Employees] to type 'PublishSubject<[Employees]>

I have successfully parsed json data using URLSession and now I want to add the parsed data to an array. Doing this using an ordinary array works fine, but I'm learning Rx and thus want to use a subject.
So, this works:
var parsedJson = [Employees]()
self.parsedJson = decodedJson.people
But this gives an error:
var parsedJson: PublishSubject<[Employees]> = PublishSubject<[Employees]>()
self.parsedJson = decodedJson.people
Cannot assign value of type '[Employees]' to type 'PublishSubject<[Employees]>'
Here is the URLSession code:
// var parsedJson = [Employees]()
var parsedJson: PublishSubject<[Employees]> = PublishSubject<[Employees]>()
func getJSON(completion: #escaping () -> Void) {
guard let url = URL(string:"https://api.myjson.com/bins/jmos6") else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
do {
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
jsonDecoder.dateDecodingStrategy = .iso8601
let decodedJson = try jsonDecoder.decode(People.self, from: data)
self.parsedJson = decodedJson.people
completion()
} catch {
print(error)
}
}.resume()
}
Anyone know how to do this and why there is an error in the first place? Doesn't the <> simply indicate which type should be observed? Didn't get .accept() to work either.
EDIT
let parsedJson: BehaviorRelay<[Employees]> = BehaviorRelay(value: [])
self.parsedJson.accept(decodedJson.people)
This worked, but what is the equivalent to BehaviorSubject and PublishSubjuct?
The error message is pretty clear: you have a type-mismatch. You would get the same error message if you tried to assign a String to an Int variable, for example. A PublishSubject is not an array. Its a mechanism (think of it as a pipeline) for sending a stream of certain types of values (here an array of Employees).
You typically use Subjects by subscribing to them like so:
var parsedJson = PublishSubject<[Employee]>()
// the 'next' block will fire every time an array of employees is sent through the pipeline
parsedJson.next { [weak self] employees in
print(employees)
}
The above next block will fire every time you send an array through the PublishSubject like so:
let decodedJson = try jsonDecoder.decode(People.self, from: data)
self.parsedJson.onNext(decodedJson.people)
From your EDIT it seems that you moved on to trying to use a BehaviorRelay. I would recommend reading up on the differences between these two classes before decided which is appropriate for your use case. This article was really helpful to me when trying to learn the differences between the different types of Subjects and Relays: https://medium.com/#dimitriskalaitzidis/rxswift-subjects-a2c9ff32a185
Good luck!
Try
self.parsedJSON.onNext(decodedJson.people)

how to make an array of urls from a folder (swift, audio)

I´m looking for a way to make an array of urls from the contents of a folder containing soundfiles so that i can call them up with AVAudioplayer later.
I use a stepper and 3 variables,
the first one is called nmbTracks to define the number of playable soundfiles, and holds the steppers maxvalue
the second is called currentActiveTrack and defines which file the player should be playing.
the 3rd one is called audioURL and is used to feed the player aswell as retrieving the name of of the file which gets outputed to a label.
the user can then step through the soundfiles with the stepper.
so far I got everything working with some files that i have but I can´t figure out how to make an array of urls nor how to get urls from the contents of a folder, next step is to let the user import their own files into that folder.
any help would be greatly appreciated, I´m sure there is an easier way to do this aswell
I have a couple of options. The first one returns an array of URLs and if there is an error fetching the folder returns an empty array.
func getFolderContentsURLs(_ folderPath: String) -> [URL] {
let fm = FileManager()
guard let contents = try? fm.contentsOfDirectory(atPath: folderPath) else { return [] }
return contents.compactMap { URL.init(fileURLWithPath: $0) }
}
You can display the result like this:
let songsURLs = getFolderContentsURLs("/")
songsURLs.forEach {
print($0.absoluteString)
}
The second option return an optional array of URLs, on error returns nil
func getFolderContentsURLsOrNil(_ folderPath: String) -> [URL]? {
let fm = FileManager()
guard let contents = try? fm.contentsOfDirectory(atPath: folderPath) else { return nil }
return contents.compactMap { URL.init(fileURLWithPath: $0) }
}
To go through the results you can do it like this:
if let songsURLs2 = getFolderContentsURLsOrNil("NotExistingPath") {
songsURLs2.forEach {
print($0.absoluteString)
}
} else {
print("Unable to fetch contents")
}
Responding to #user12184258 comment about returning only audio files you can apply a filter after getting the folder contents. The filter can check each file using UTTypeConformsTo from MobileCoreServices
let contentsFolder = getFolderContentsURLs("/path/to/folder")
let audioFiles = contentsFolder
.filter { path in
// Try to create uniform type identifier using file's extension
guard let uti =
UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension,
path.pathExtension as CFString, nil) else { return false }
// Checks conformance of UTI with kUTTypeAudio (abstract type for audio)
return UTTypeConformsTo((uti.takeRetainedValue()), kUTTypeAudio)
}
I found this method here. Also Apple has a very useful System-Declared Uniform Type Identifiers you can check, if you want only mp3 files you can use kUTTypeMP3.

How do I append elements to a global array (in a for loop) in Swift?

I have an empty global array. The only simple thing I want to do is add an element to this array. It seems in swift this seemingly simple task is proving to be difficult. I am just left with an empty array and nothing is appending to my global array.
I can see that it prints out values in the for loop. So the values are actually there.
This is some stuff I have declared globally (Yes, I know global variables are bad but I will sort that out later):
struct HouseDetails: Decodable {
let median_price: String
let sale_year: String
let transaction_count: String
let type: String
}
var hsArray: [HouseDetails] = []
and in the viewDidLoad() function I have the data which I am storing in local variable "houses". When I loop through the array it prints median_price, showing that the values are there.
However when I do hsArray.append(h) it seems to do nothing.
let jsonUrlString = "https://data.melbourne.vic.gov.au/resource/i8px-csib.json"
guard let url = URL(string: jsonUrlString)
else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let houses = try JSONDecoder().decode([HouseDetails].self, from: data)
for h in houses {
hsArray.append(h)
print(h.median_price)
}
}
catch let jsonErr {
print("Error with json serialization", jsonErr)
}
}.resume()
Thank you for any help. In other languages I am used to being able to append an element to the end of an existing array, so I am sure it is just a small error.
Firstly, why don't you simply do
hsArray.append(contentsOf: houses)
instead of all that for loop
for h in houses {
hsArray.append(h)
print(h.median_price)
}
The issue might be the time at which you are using hsArray. See if the response is received after you use hsArray.

Swift complains of undeclared type, but that doesn't seem to be the problem

I am trying to fetch records in Core Data.
func fetchOrg() {
var **internalOrganization** = [InternalOrganizationMO]() //NSManagedClass
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "InternalOrganization")
fetchRequest.returnsObjectsAsFaults = false
do {
let result = try managedObjectContext.fetch(fetchRequest) as **internalOrganization** **////Compiler flags Error here**
} catch {
fatalError("Failed to fetch internal organization \(error)")
}
}
InternalOrganizationMO is a ManagedObject Class corresponding to the object model and it seems clear to me that internalOrganization is declared to be an array of those objects, so the flagged error seems to be off. My understanding is that this is the kind of object that is supposed to be the target of a fetch, but I am definitely on the learning curve here.
Is it that the fetch needs to be targeted at a Type instead of an array--thus, the complaint about my not providing a named type? If that is it, am I simply supposed to provide the ManagedObject? If that is so, how on earth do I determine how many records are returned?
Is this really better than just using the interface to SQLite?
Thanks, sorry for the rant.
You typecast objects as types, not objects as objects.
Example:
let a = b as! [String]
and not:
let a = [String]()
let c = b as! a
Solution #1:
Change NSFetchRequest<NSFetchRequestResult> to specify the type to be explicitly InternalOrganizationMO, like so:
NSFetchRequestResult<InternalOrganizationMO>
This fetchRequest has now the proper associated type InternalOrganizationMO and will be used accordingly to return objects of this type.
You then won't need to typecast result again and the following code should work just fine:
func fetchOrg() {
let fetchRequest = NSFetchRequest<InternalOrganizationMO>(entityName: "InternalOrganization")
do {
let internalOrganization = try managedContext.fetch(fetchRequest)
/*
internalOrganization will be of type [InternalOrganizationMO]
as that is the return type of the fetch now.
*/
//handle internalOrganization here (within the do block)
print(internalOrganization.count)
}
catch {
fatalError("Failed to fetch internal organization \(error)")
}
}
Solution #2:
If you want this method to work even if the fetchRequest try or typecasting fails then you can do this:
func fetchOrg() {
var internalOrganization = [InternalOrganizationMO]()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "InternalOrganization")
do {
internalOrganization = try managedContext.fetch(fetchRequest) as? [InternalOrganizationMO]
/*
You ofcourse wouldn't want the above optional binding to fail but even if it
does, atleast your method can stay consistent and continue with an empty array
*/
}
catch {
fatalError("Failed to fetch internal organization \(error)")
}
//handle internalOrganization here
print(internalOrganization.count)
}
The choice of solution, depends on your design and requirements.

Resources