Update Plist data without erasing old data - ios

I'm trying to make a file downloader application using swift and cocoa. I am using plist for the download history. Reading the data works however, writing the data will erase the previous data and replace it for the new data.
Here is the code
let newdownloaditem = downloadList(root: [downloadListt(downloadURL: response.url!.absoluteString, fileName: response.suggestedFilename!)])
// This is a codeable method
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
let pListFilURL = uniqueDataDir()?.appendingPathComponent("downloads.plist")
do {
let data = try encoder.encode(newdownloaditem)
try data.write(to: pListFilURL!)
// Here is the problem
} catch {
print(error)
}
// Here is the codeable
public struct downloadList: Codable {
let root: [downloadListt]
}
public struct downloadListt: Codable {
let downloadURL: String
let fileName: String
}
Here is an image of what happens
The contents got erased
Thanks!

You are indeed replacing the previous data with your new one.
You need to retrieve the previous data.
Append your new data to it.
Save that combination
let newItem = downloadListt(downloadURL: response.url!.absoluteString,
fileName: response.suggestedFilename!)
var allItems: [downloadListt] = []
allItems.append(contentsOf: previousList.root)
allitems.append(newItem)
let newList = downloadList(root: allItems)
...
let data = try encoder.encode(newList)
try data.write(to: pListFilURL!)
Unrelated but recommended (it's convention):
You should starting naming your struct/classes with an uppercase: downloadList => DownloadList
I would avoid naming downloadListt, it's unredable it's hard at first look to make a difference between downloadListt and downloadList. Instead, name it maybe DownloadItem. More readable.

Related

Swift ZIPFoundation extract string in memory is not working

I am using ZipFoundation in Swift from https://github.com/weichsel/ZIPFoundation
My requirement is unzip the file contents in memory and directly convert into String.
let archive = Archive(url: fileUrl, accessMode: .read, preferredEncoding: .utf8)
do{
try archive?.extract(entry, consumer: {data in
print(data.count)
})
}catch{}
The archive object is always null its not reading zip file. Also what is the entry object to pass to extract method?
ZIP Foundation archives support subscripting. This allows you to obtain an Entry by subscripting into an Archive via archive["path/to/file.txt"].
To get access to the contents of the obtained file, you use the closure-based version of extract as follows:
guard let archiveURL = Bundle.main.url(forResource: "archive", withExtension: "zip"),
let archive = Archive(url: archiveURL, accessMode: .read),
let entry = archive["test/data.random"]
else { return }
_ = try? archive.extract(entry) { data in
print(data.count)
}

Storing OpaquePointer type in UserDefaults

I am working on Xcode 12 and Swift 5 environment to build an iOS application.
I need to store an OpaquePointer type variable("self.loggers" in the code below) before the view disappears(or before the app closes) and retrieve it when the view appears( when the app runs).
I tried to use UserDefault as below,
// Storing(In viewWillDisappear)
do {
encodedData = try NSKeyedArchiver.archivedData(withRootObject: self.loggers, requiringSecureCoding: false)
UserDefaults.standard.set(encodedData, forKey: "accelLoggerID")
} catch {
print("archiving error")
}
...
// Retrieving(In viewDidLoad)
if let decodedData = UserDefaults.standard.object(forKey: "acceleration") as? Data {
do {
self.loggers = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decodedData) as? [String:OpaquePointer] ?? [:]} catch {
print("unarchiving error")
}
} else {
self.loggers = [:]
print("No value in Userdefault. [viewDidLoad]")
}
However, NSKeyedArchiver failed to encode such type. After that, I made a class that wraps the variable.
class LoggerWrapper {
var loggers: [String : OpaquePointer]
init(loggerDic: [String : OpaquePointer]) {
loggers = loggerDic
}
}
And changed my code like
self.loggers = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decodedData) as? LoggerWrapper ?? LoggerWrapper(loggerDic: [:])} catch {
print("unarchiving error")
}
However, this gives SIGABRT when NSKeyedArchiver.archivedData is called.
Is there any way to store Opaquepointer type in UserDefaults? If not, can using core data solve this problem?
Thank you.
You probably need to convert the pointers to Int bitPatterns. There is an initializer on Int for that: let intBitPattern = Int(bitPattern: opaquePointer).
There is also an initializer on OpaquePointer that takes an Int bitPattern: let opaquePointer = OpaquePointer(bitPattern: intBitPattern).
So you would need to convert the values in your dictionary (e.g. using compactMapValues) before storing and after reading it.
With a Dictionary<String, Int>, you should be able to store it directly in UserDefaults, without the need for NSKeyedArchiver and NSKeyedUnarchiver.
Sidenote:
Pointers aren't "long-lived". So storing it in UserDefaults is only valid during the lifetime of your app. Once your app restarts, the pointers might no longer be valid.

In swift, is there a way to add new data from second Realm bundle, without overwriting any existing data in the current Default Realm?

Upon initial load of the app, the Bundled Realm (Realm1) is copied to the documents folder. Now that the bundled realm is set as the default realm, I am able update the bool property so that the table view can show marked and unmarked cells. However I am looking for a way to bundle a second realm (Realm2) with a later update, that will add new data to the existing default realm, but without overwriting the current default realm. I am currently working in swift 5 and Xcode 11.1, if that is helpful.
So far the only thing that I can think of is adding block of code to add new entries to the default realm. First the view will check to see what the count is of the realm, and if the count is the same as the original bundle, then it will add new data, if the count is equal to the initial bundle plus the new entries, then it will not add the new data again. I was hoping for a simpler solution that is cleaner in my opinion.
Ideally the end result would be a way to update the existing default realm, without overwriting the already edited content. Although I am rather new to using realm, any help in pointing me in the right direction for a solution would be greatly appreciated. Thanks.
Attached below is the current code I have implemented to load the default realm from the bundle.
let bundlePath = Bundle.main.path(forResource: "preloadedData", ofType: "realm")!
let defaultPath = Realm.Configuration.defaultConfiguration.fileURL!.path
let fileManager = FileManager.default
// Copy Realm on initial launch
if !fileManager.fileExists(atPath: defaultPath){
do {
try fileManager.copyItem(atPath: bundlePath, toPath: defaultPath)
print("Realm was copied")
} catch {
print("Realm was not coppied \(error)")
}
}
return true
Once you've created your default Realm on disk, if you want to read data from the bundled one, here's the code
let config = Realm.Configuration(
// Get the URL to the bundled file
fileURL: Bundle.main.url(forResource: "MyBundledData", withExtension: "realm"),
// Open the file in read-only mode as application bundles are not writeable
readOnly: true)
let realm = try! Realm(configuration: config)
and once you've read the data, you can switch back
var config = Realm.Configuration()
config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("\(some_realm_name).realm")
Realm.Configuration.defaultConfiguration = config
as far as not overwriting, ensure your objects use a unique primary key and when they are written, nothing will be overwritten as objects will a unique primary key will be added instead of overwriting.
class MyClass: Object {
#objc dynamic var my_primary_id = NSUUID().uuidString
I am adding an additional answer that's somewhat related to the first but also stands on it's own.
In a nutshell, once Realm connects to a data source, it will continue to use that data source as long as the objects are not released, even if the actual file is deleted.
The way around that is to encapsulate the Realm calls into an autorelease pool so that those objects can be released when the Realm is deleted.
Here’s an example:
This function adds a GameData object to the default.realm file.
func addAnObject() {
autoreleasepool {
let realm = try! Realm()
let testData = GameData()
testData.Scenario = "This is my scenario"
testData.Id = 1
try! realm.write {
realm.add(testData)
}
}
}
At this point, if you run the addAnObject code, your file will have a GameData object.
GameData {
Id = 1;
GameDate = (null);
Scenario = This is my scenario;
GameStarted = 0;
}
Then a function that delete’s the old realm, and copies the bundled realm to it’s place. This works because all of the interaction with Realm was enclosed in an autorelease pool so the objects can be released.
func createDefaultRealm() {
let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
let defaultParentURL = defaultURL.deletingLastPathComponent()
if let bundledRealmURL = self.bundleURL("default") {
do {
try FileManager.default.removeItem(at: defaultURL)
try FileManager.default.copyItem(at: bundledRealmURL, to: defaultURL)
} catch let error as NSError {
print(error.localizedDescription)
return
}
}
let migrationBlock : MigrationBlock = { migration, oldSchemaVersion in
//handle migration
}
Realm.Configuration.defaultConfiguration = Realm.Configuration(schemaVersion: 18, migrationBlock: migrationBlock)
print("Your default realm objects: \(try! Realm().objects(GameData.self))")
}
func bundleURL(_ name: String) -> URL? {
return Bundle.main.url(forResource: name, withExtension: "realm")
}
and please note that if you access Realm inside the class but outside an autorelease pool, Realm will refuse to 'let go' of it's objects.
Do do NOT do this!!
class ViewController: UIViewController {
var realm = try! Realm()

How can I take out a value as a string from RealmSwift?

I am developing an iOS app with RealmSwift by referring here. https://realm.io/docs/swift/latest/#in-memory-realms
And, what I don't understand is, how can I indicate the location(record and column) of the data in the realm file.
I've saved a realm file that named "DicData.realm" on the main folder where the same location as ViewController.swift is saved.
The data of DicData.realm is something like this:
1,face,423
2,rain,435
3,airplane,555
If I run the code below, it only printed like this: "results: Results ( )". It seems the filter method is just neglected. When I want to take out the word "airplane" and store in a variable as a string, how should I modify my code?
override func didMoveToView(view: SKView) {
func test()->Int {
let realm = try! Realm(configuration: Realm.Configuration(inMemoryIdentifier: "DicData"))
let results = realm.objects(DBData).filter("id == 3")
print("results: \(results)")
}
class DBData: Object {
dynamic var id = 0
dynamic var name = ""
dynamic var code = ""
}
You're referring here to the chapter of in-memory Realms and have setup your configuration to use those. An in-memory Realm is not a file. It lives exclusively in memory and is not actually written to disk.
If you've a prepared file, you want to bundle with your app, you need to make sure, that it is part of the Copy-Files Build Phase of the corresponding target of your Xcode project.
You can then copy the file from the app bundle initially via NSFileManager to a path, where the copy can be modified.
let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
let bundleURL = NSBundle.mainBundle().URLForResource("DicData", withExtension: "realm")!
do {
try NSFileManager.defaultManager().copyItemAtURL(bundleURL, toURL: defaultURL)
} catch {}
}

Prevent Realm from writing JSON twice

I'm new to Realm and iOS. I'm working on an app(written in Swift) that has a feature for the user to search for golf courses. I have a JSON file with roughly 18K courses in it. So I wanted to use Realm so I can quickly search through these courses in my app without it slowing down the user experience. I was able to get my JSON file written to the Realm Browser and can retrieve and search through the items, which has made it a LOT faster.
The problem I'm is I have the code in my App Delegate because I wanted to send the JSON items to my Realm Browser upon the app's launch. But if the app is started again then it writes the JSON file again, which creates duplicate golf courses in my Realm Browser.
Any suggestions on how I should do this so I can write the JSON file to the browser without getting duplicates each time the app is launched?
Thanks!
My code for writing my JSON file to my Realm Browser:
let dataManager = DataManager.getGolfCoursesFromFileWithSuccess { (data) -> Void in
let json = JSON(data: data)
if let courseArray = json.array {
for course in courseArray {
let golfCourseName: String? = course["biz_name"].string
let city: String? = course["e_city"].string
let state: String? = course["e_state"].string
if golfCourseName != nil {
let course = Course()
course.name = golfCourseName!
course.city = city!
course.state = state!
let realm = try! Realm()
try! realm.write {
realm.add(course)
}
}
}
}
}
Just check to see if the data is stored already.
I.e.
if try! Realm().objects(GolfCourse).count == 0 {
// your loading code here.
}
I figured it out. Because this was a data set that I wanted the user to have when they initially start using the app I learned how to bundle a Realm file with this data and put the file directly in my Xcode project. I then configured this file using the method:
"NSBundle.mainBundle().pathForResource("MyBundledData", ofType:"realm")"

Resources