Swift detect in the setter which object in an array was changed - ios

I have an array of Habit objects which I am modifying in memory and a getter/setter to read and write it to documents storage as JSON whenever changes are made. I have split each object into its own JSON file and so far everything works, however changing one object in the array will re-write all the files. I understand this is what I instructed my setter to do, but is there a way to only write the objects in newValue that were changed?
extension Storage {
static var habits: [Habit] {
get { ... }
set {
newValue.forEach({
let data = try! JSONEncoder().encode($0)
do { try data.write(to: Storage.habitsFolder.appendingPathComponent("habit-\($0.id).json")) }
catch let error {
print("Failed to write Habit \($0.id): \(error.localizedDescription)")
}
})
}
}
}
The way I would make a change now is Storage.habits[0].name = "Some name". Which calls the setter, which then re-writes the files for each habit. So I was wondering if there's some way to detect which part of newValue changed or pass an index to it and only update that file.
Is the only way to go about this to have the array be get-only and use a different method for setting each habit file?
Thank you, and apologies if this is a silly question.
Update: adding Habit class for more context
class Habit: Codable, CustomStringConvertible {
// MARK: - Properties
var id: Int
var name: String?
var notes: String?
var icon: String
var repeatPattern: RepeatPattern
var entries: [HabitEntry]
var reminders: [Reminder]
init(id: Int, name: String?, notes: String?, icon: String, entries: [HabitEntry], reminders: [Reminder]) {
self.id = id
self.name = name
self.notes = notes
self.icon = icon
self.repeatPattern = RepeatPattern(pattern: 0, startDay: calendar.today, enabledWeekdays: [1,2,3,4,5,6,7], expectedTimes: 1)
self.entries = entries
self.reminders = reminders
}
var description: String { return "Habit id: \(id), named: \(name ?? "nil"), \nnotes: \(notes ?? "nil"), \nicon: \(icon), \nrepeatPattern: \(repeatPattern), \nentries: \(entries), \nreminders: \(String(describing: reminders))" }
}

You could make a property to store the existing habits first
class Storage {
static var existingHabits = [Habit]()
}
Then, inside the set, see which Habits are new:
static var habits: [Habit] {
get { ... }
set {
var habitsToChange = [Habit]()
if existingHabits.count == 0 { /// it's empty, just write with newValue
habitsToChange = newValue
} else {
let differentIndicies = zip(existingHabits, newValue).enumerated().filter() {
$1.0 != $1.1
}.map{$0.0} /// from https://stackoverflow.com/a/30685226/14351818
for index in differentIndicies {
habitsToChange.append(newValue[index])
}
}
habitsToChange.forEach({ /// loop over the habits that have changed
do {
try JSONEncoder().encode($0).write(to: habitsFolder.appendingPathComponent("habit-\($0.id).json"))
} catch {
print("Failed to write Habit \($0.id): \(error)")
}
})
existingHabits = newValue /// set existing to the new value
}
}

Related

Realm Swift duplicates item with primary key -> Threading problem?

I'm using ReamSwift to store data in my iOS application and I have troubles to figure out, why sometimes objects are duplicated in my application.
I use an UUID as primary key on every object. This property is defined in a base class and all the others are subclasses of that.
I use the update approach with a dictionary. So new objects are created and existing should be updated.
I use a central class which holds the Realm object and handles the update. This method could be called from multiple different threads.
This is my base class
import RealmSwift
open class RealmObject : Object {
static let ID_ATTRIBUTE : String = "id"
#objc dynamic var id : String = ""
override public static func primaryKey() -> String? {
return "id"
}
}
Central class managing swift.
This class holds the Realm Object
public var realm: Realm {
var tempRealm: Realm?
do {
tempRealm = try Realm(configuration: configuration)
}
catch let err {
print("------ERROR----- initializing realm\n \(err)")
}
if let tempRealm = tempRealm{
return tempRealm
}
return self.realm
}
This is the update method in the class. As a fallback if the id property is not set, it will be set as it is the primary key
func update<T: RealmObject>(_ obj : T, values : [String:Any?] ) -> T? {
var vals = values
do {
var res : T?
try realm.write {
if let idattr = vals[T.ID_ATTRIBUTE] as? String {
if(idattr.isEmpty) {
vals[T.ID_ATTRIBUTE] = UUID().uuidString
}
} else {
vals[T.ID_ATTRIBUTE] = UUID().uuidString
}
res = realm.create(T.self, value: vals, update: .modified)
}
return res
} catch {
return nil
}
}
Could calling the update method cause in any case the duplication as I set the primary key of the object? The problem is, I cannot reproduce to find the problem, i just encounter the issue from time to time and in the field from users.
One interesting thing, when a copy is deleted, also the other object will be deleted.
This is the method which deletes objects by id and type
func delete<T: RealmObject>(_ id : String, _ type : T.Type) -> Bool {
do {
let obj = get(id, T.self)
if let obj = obj {
try realm.write {
realm.delete(obj)
}
return true
} else {
return false
}
} catch {
return false
}
}

Issue with adding Data to an AnyObject Var so that I could make native ads work

for postdata in postdata {
if index < tableViewItems.count {
tableViewItems.insert(postdata, at: index)
index += adInterval
} else {
break
}
}
I'll need to add both PostData ads and Native Ads on the same AnyObject Var for me to get it to work and I can't find a way to add the Post Data because it says an error appears saying "Argument type 'PostData' expected to be an instance of a class or class-constrained type." Assistance would be very much appreciated, thank you.
edit 1
class Ad {
var postimage: String!
var publisher: String!
var timestring: String!
var timestamp = Date().timeIntervalSince1970
}
class PostDataAd: Ad {
// Declare here your custom properties
struct PostData1
{
var postimage: String
var publisher: String
var timestring : String
var timestamp = Date().timeIntervalSince1970
}
}
class NativeAd: Ad {
// Declare here your custom properties
struct NativeAd
{
var nativeAds: [GADUnifiedNativeAd] = [GADUnifiedNativeAd]()
}
}
My model class to merge both Data into one AnyObject Var
and then trying to append the Data from Firebase by doing this
var ads: [Ad] = [PostDataAd(), NativeAd()]
let postList = PostDataAd.PostData1(postimage: postimage, publisher:
postpublisher, timestring: postid, timestamp: timestamp)
self.ads.insert(postList, at:0)
an error occurs saying Cannot convert value of type 'PostDataAd.PostData1' to expected argument type 'Ad'
I hope I got what you want correctly. So basically you have two objects which you want to store in one array, under AnyObject. If that is correct, I recommend you to go in a bit of different direction. It is a nice example where you can use subclassing. You can declare a class called Ad, where you define the common properties which will be true for both PostDataAds and NativeAds.
class Ad {
// Add here the common elements you want to retrieve from both classes
var name: String = ""
}
After you define your PostDataAds and NativeAds inheriting from Ad:
class PostDataAd: Ad {
// Declare here your custom properties
}
class NativeAd: Ad {
// Declare here your custom properties
}
And if you want to define an array with two types of objects you can go:
let ads: [Ad] = [PostDataAd(), NativeAd()]
When retrieving you can check their type:
if let item = ads.first as? PostDataAd {
// The Ad is a PostDataAd
} else if let item = ad as? NativeAd {
// The Ad is a NativeAd
}
Or at some cases you don't even how to know the exact type, as you can access the properties defined in Ad without checking.
Update:
First of all your PostData1 and Ad objects are the same, you don't need to duplicate them. If you really want to have two classes you can inherit PostData1 from Ad.
class Ad {
var postimage: String
var publisher: String
var timestring: String
var timestamp = Date().timeIntervalSince1970
// You can add timestamp here also if you wish
init(postimage: String, publisher: String, timestring: String) {
self.postimage = postimage
self.publisher = publisher
self.timestring = timestring
}
}
class PostDataAd: Ad {
// Define some custom properties
}
And if you want to append PostData to the [Ad] array, you would do the following:
var ads: [Ad] = []
// Replace with your values
let postList = PostDataAd(postimage: "", publisher: "", timestring: "")
ads.insert(postList, at: 0)
// Appending NativeAd works also
let nativeAdd = NativeAd(postimage: "", publisher: "", timestring: "")
ads.append(nativeAdd)

How to design a Settings model to generically get + set values for all UserDefaults keys

I’m setting up my Settings class which gets/sets values from UserDefaults. I wish for as much of the code to be generic to minimise effort involved whenever a new setting is introduced (I have many as it is, and I expect many more in the future too), thereby reducing the probability of any human errors/bugs.
I came across this answer to create a wrapper for UserDefaults:
struct UserDefaultsManager {
static var userDefaults: UserDefaults = .standard
static func set<T>(_ value: T, forKey: String) where T: Encodable {
if let encoded = try? JSONEncoder().encode(value) {
userDefaults.set(encoded, forKey: forKey)
}
}
static func get<T>(forKey: String) -> T? where T: Decodable {
guard let data = userDefaults.value(forKey: forKey) as? Data,
let decodedData = try? JSONDecoder().decode(T.self, from: data)
else { return nil }
return decodedData
}
}
I’ve created an enum to store all the setting keys:
enum SettingKeys: String {
case TimeFormat = "TimeFormat"
// and many more
}
And each setting has their own enum:
enum TimeFormat: String, Codable {
case ampm = "12"
case mili = "24"
}
In this simplified Settings class example, you can see that when it’s instantiated I initialise the value of every setting defined. I check if its setting key was found in UserDefaults: if yes, I use the value found, otherwise I set it a default and save it for the first time to UserDefaults.
class Settings {
var timeFormat: TimeFormat!
init() {
self.initTimeFormat()
}
func initTimeFormat() {
guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
self.setTimeFormat(to: .ampm)
return
}
self.timeFormat = format
}
func setTimeFormat(to format: TimeFormat) {
UserDefaultsManager.set(format, forKey: SettingKeys.TimeFormat.rawValue)
self.timeFormat = format
}
}
This is working fine, and pretty straightforward. However, thinking ahead, this will be tedious (and therefore error-prone) to replicate for every setting in this app (and every other I look to do in the future). Is there a way for the init<name of setting>() and set<name of setting>() to be generalised, whereby I pass it a key for a setting (and nothing more), and it handles everything else?
I’ve identified every setting to have these shared elements:
settings key (e.g. SettingsKey.TimeFormat in my example)
unique type (could be AnyObject, String, Int, Bool etc. e.g. enum TimeFormat in my example)
unique property (e.g. timeFormat in my example)
default value (e.g. TimeFormat.ampm in my example)
Thanks as always!
UPDATE:
This may or may not make a difference, but considering all settings will have a default value, I realised they don’t need to be a non-optional optional but can have the default set at initialisation.
That is, change:
var timeFormat: TimeFormat!
func initTimeFormat() {
guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
self.setTimeFormat(to: .ampm)
return
}
self.timeFormat = format
}
To:
var timeFormat: TimeFormat = .ampm
func initTimeFormat() {
guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
UserDefaultsManager.set(self.timeFormat, forKey: SettingKeys.TimeFormat.rawValue)
return
}
self.timeFormat = format
}
The setTimeFormat() function is still needed when its value is changed by the user in-app.
Note that your settings don't have to be stored properties. They can be computed properties:
var timeFormat: TimeFormat {
get {
UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) ?? .ampm
}
set {
UserDefaultsManager.set(newValue, forKey: SettingKeys.TimeFormat.rawValue)
}
}
You don't have to write as much code this way, when you want to add a new setting. Since you will just be adding a new setting key enum case, and a computed property.
Further more, this computed property can be wrapped into a property wrapper:
#propertyWrapper
struct FromUserDefaults<T: Codable> {
let key: SettingKeys
let defaultValue: T
init(key: SettingKeys, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
UserDefaultsManager.get(forKey: key.rawValue) ?? defaultValue
}
set {
UserDefaultsManager.set(newValue, forKey: key.rawValue)
}
}
}
Usage:
class Settings {
#FromUserDefaults(key: .TimeFormat, defaultValue: .ampm)
var timeFormat: TimeFormat
}
Now to add a new setting, you just need to write a new enum case for the key, and two more lines of code.

How to access & get nested values from IOS Swift 'Any' type?

I am trying to read from Firestore into a Dictionary[Any] type using Struct. I can get the values loaded into variable "data" dictionary with Any type.
However I cannot loop thru it to access normal nested Dictionary variable.
I cannot get Key, values printed.
Following is my code:
class PullQuestions {
//shared instance variable
**public var data = [Any]()**
private var qdb = Firestore.firestore()
public struct questionid
{
let qid : String
var questions : [basequestion]
var answers: [baseans]
}
public struct basequestion {
let category : String
let question : String
}
public struct baseans {
let answer : String
}
class var sharedManager: PullQuestions {
struct Static {
static let instance = PullQuestions()
}
return Static.instance
}
static func getData(completion: #escaping (_ result: [Any]) -> Void) {
let rootCollection = PullQuestions.sharedManager.qdb.collection("questions")
//var data = [Any]()
rootCollection.order(by: "upvote", descending: false).getDocuments(completion: {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
guard let topSnapshot = querySnapshot?.documents else { return }
// var questiondoc = [basequestion]()
for questioncollection in topSnapshot {
rootCollection.document(questioncollection.documentID).collection("answers").getDocuments(completion: {
(snapshot, err) in
guard let snapshot = snapshot?.documents else { return }
var answers = [baseans]()
for document in snapshot { //There should be only one Document for each answer collection
//Read thru all fields
for i in 0..<document.data().count
{
let newAns = baseans(answer: answer)
print("Answer Docs=>", (answer))
answers.append(newAns)
}
}
let qid = questioncollection.documentID
let category = questioncollection.data()["category"] as! String
let question = questioncollection.data()["question"] as! String
let newQuestions = basequestion(category: category ,question: question)
let newQuestionDict = questionid(qid: qid, questions: [newQuestions], answers: answers)
PullQuestions.sharedManager.data.append(newQuestionDict)
//Return data on completion
completion(PullQuestions.sharedManager.data)
})
}
}
})
}
}
I can print like this
print("Count =>", (PullQuestions.sharedManager.data.count))
// print(PullQuestions.sharedManager.data.first ?? "Nil")
print(PullQuestions.sharedManager.data[0])
for element in PullQuestions.sharedManager.data
{
print("Elements in data:=>", (element))
}
I could access only the key.. how do i go and get the nested values ?
First of all, consider using Swift code conventions (e.g. your structs are named with small letters, but you should start with capital), this will make your code more readable.
Returning to your question. You use an array instead of dictionary (this piece of code: public var data = [Any]()). And here you are trying to print values:
for element in PullQuestions.sharedManager.data
{
print("Elements in data:=>", (element))
}
In this context element is an Any object, thus you cannot access any underlying properties. In order to do this you have two options:
1. You should specify the type of array's objects in it's declaration like this:
public var data = [questionid]()
or you can user this:
public var data: [questionid] = []
These two are equals, use the one you prefer.
2. If for any reasons you don't want to specify the type in declaration, you can cast it in your loop. Like this:
for element in PullQuestions.sharedManager.data
{
if let element = element as? quetionid {
print("Elements in data:=>", (element))
// you can also print element.qid, element.questions, element.answers
} else {
print("Element is not questionid")
}
}
You could of course use the force cast:
let element = element as! questionid
and avoid if let syntax (or guard let if you prefer), but I wouldn't recommend this, because it (potentially) can crash your app if element will be nil or any other type.

Is there any way of sharing getter and setter through properties in Swift?

I'm building a helper to enable typed access to NSUserDefaults properties. Something like this:
struct UserDefaults {
private static var standardUserDefaults: NSUserDefaults = {
return NSUserDefaults.standardUserDefaults()
}()
private static let propKey = "PROP"
static var prop: Bool {
get {
return standardUserDefaults.boolForKey(propKey)
}
set {
standardUserDefaults.setBool(newValue, forKey: propKey)
standardUserDefaults.synchronize()
}
}
}
This way I can have a nice syntax for reading and writing to NSUserDefaults:
UserDefaults.prop // read
UserDefaults.prop = false // write
The problem is that there's a lot of boilerplate code for this, I need 10 lines for each aditional property.
Is there any way of reducing the amount of lines needed for each new property? Reusing getter and setter? Any kind of run time generator?
You can try wrapping the actual value in a class that handles all the dirty work for you:
class WrappedUserDefault<T> {
let key : String
let defaultValue : T
var value : T {
get {
if let value = UserDefaults.standardUserDefaults.objectForKey(key) as? T {
return value
} else {
return defaultValue
}
}
set {
if let value = newValue as? AnyObject {
UserDefaults.standardUserDefaults.setValue(value, forKey: key)
} else {
UserDefaults.standardUserDefaults.removeObjectForKey(key)
}
UserDefaults.standardUserDefaults.synchronize()
}
}
init(key:String, defaultValue:T) {
self.key = key
self.defaultValue = defaultValue
}
}
struct UserDefaults {
static let standardUserDefaults = NSUserDefaults.standardUserDefaults()
static let ready = WrappedUserDefault<Bool>(key:"ready", defaultValue: true)
static let count = WrappedUserDefault<Int>(key: "count", defaultValue: 0)
}
Then with just a little bit more code you wind up with:
UserDefaults.count.value++
UserDefaults.ready.value = true
UserDefaults.ready.value
If the verbosity of ready.value bothers you, you can somewhat hide that, although then you're back to you're back to having a fair amount of copy/paste code:
struct UserDefaults {
static let standardUserDefaults = NSUserDefaults.standardUserDefaults()
private static let readyWrapper = WrappedUserDefault<Bool>(key:"ready", defaultValue: true)
static var ready : Bool {
get { return readyWrapper.value }
set { readyWrapper.value = newValue }
}
}
At least in this case though, the copy/paste code is fairly trivial, so unlikely to need to be altered in the future.
I like David's answer much better, but here's another option. Drops your 10 lines per variable down to 5 (mainly because of new line removal...)
struct UserDefaults {
private static var standardUserDefaults: NSUserDefaults = {
return NSUserDefaults.standardUserDefaults()
}()
//Repeate these 5 lines for all new variables,
//changing the as? to the proper variable type
//Adding in a default value for the return in
//case the as? cast fails for any reason
private static let propKey = "PROP"
static var prop: Bool {
get { return (getVar(propKey) as? Bool) ?? false }
set { setVar(newValue, key:propKey) }
}
//The generic set/get
private static func getVar(key : String) -> AnyObject?
{
return standardUserDefaults.objectForKey(key)
}
private static func setVar(newValue : AnyObject, key : String)
{
if(newValue is Bool)
{
standardUserDefaults.setBool((newValue as? Bool)!, forKey: key)
}
//... More cases here
else if(newValue == nil)
{
standardUserDefaults.removeObjectForKey(key)
}
else
{
standardUserDefaults.setObject(newValue, forKey: key)
}
standardUserDefaults.synchronize()
}
}

Resources