Swift Firebase Reuse a function throughout the app - swift

In my app, in a few places I am loading data from Firebase Firestore database and showing the data. The problem is I am not adopting the DRY technique and I know I shouldn't, but I am reusing this same load function in different places in my app.
func loadData() {
let user = Auth.auth().currentUser
db.collection("users").document((user?.uid)!).collection("children").getDocuments() {
QuerySnapshot, error in
if let error = error {
print("\(error.localizedDescription)")
} else {
// get all children into an array
self.childArray = QuerySnapshot!.documents.flatMap({Child(dictionary: $0.data())})
DispatchQueue.main.async {
self.childrenTableView.reloadData()
}
}
}
}
The function simply grabs all the children from the database and adds them to my child array.
Is there some better way to do this or a central place I can put this function where it can be called as and when I need it in the app instead of repeatedly adding it in multiple view controllers?
I thought about a helper class, and just calling the function, but then not sure how to add the result to the childArray in the viewcontroller I needed it?
my Child model
import UIKit
import FirebaseFirestore
protocol DocumentSerializable {
init?(dictionary:[String:Any])
}
// Child Struct
struct Child {
var name: String
var age: Int
var timestamp: Date
var imageURL: String
var dictionary:[String:Any] {
return [
"name":name,
"age":age,
"timestamp":timestamp,
"imageURL":imageURL
]
}
}
//Child Extension
extension Child : DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let age = dictionary["age"] as? Int,
let imageURL = dictionary["imageURL"] as? String,
let timestamp = dictionary["timestamp"] as? Date else {
return nil
}
self.init(name: name, age: age, timestamp: timestamp, imageURL: imageURL)
}
}

EDIT: I have updated to safely unwrap the optionals. You may still have to modify as I am not sure what your Firebase structure is, nor do I know your Child initializer.
You could just write this as a static function and then reuse it everywhere. I assume you might have some class related to whatever "children" is, and that'd be the best place to implement. You could pass the results (as an option array of Child) in a completion handler so that you can do whatever you need with those results. It'd look something like this:
static func loadData(_ completion: (_ children: [Child]?)->()) {
guard let user = Auth.auth().currentUser else { completion(nil); return }
Firestore.firestore().collection("users").document(user.uid).collection("children").getDocuments() {
querySnapshot, error in
if let error = error {
print("\(error.localizedDescription)")
completion(nil)
} else {
guard let snapshot = querySnapshot else { completion(nil); return }
// get all children into an array
let children = snapshot.documents.flatMap({Child(dictionary: $0.data())})
completion(children)
}
}
}
Assuming you have this implemented in your Child class you would use it like this:
Child.loadData { (children) in
DispatchQueue.main.async {
if let loadedChildren = children {
//Do whatever you need with the children
self.childArray = loadedChildren
}
}
}

Related

Any clean way for batch inserting coredata objects with relationships?

I have been observing high CPU times in background threads while inserting the coredata objects, and from analyser i could find that it's coming majorly because of some relationships i was creating one by one, and those could be in thousands.
So i thought if i could create them with batch insert. I can do that easily for objects using without relationships using NSBatchInsertRequest, but with relationships, I can't seem to find any clean way.Without relationships, i can easily create dictionaries and insert using the above request.
​
With relationships, i also tried using the object handler method of NSBatchInsertRequest, but even that is giving me an exception
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Illegal attempt to establish a relationship 'run' between objects in different contexts
This is how i am trying to make sure that the trackpoint getting added is using the run object from the same context as the one in which its being created
func addTrackPoints(run: RunModel, objectId: NSManagedObjectID) async throws {
let locations:[CLLocation] = run.getLocations()
let count = run.getLocations().count
var index = 0
let batchInsert = NSBatchInsertRequest(entity: TrackPoint.entity()) { (managedObject: NSManagedObject) -> Bool in
guard index < count else { return true }
if let trackPoint = managedObject as? TrackPoint {
let data = locations[index]
guard let run = try? StorageService.shared.getBackgroundContext().object(with: objectId) as? Run else {
fatalError("failed to get run object")
}
trackPoint.run = run
}
index += 1
return false
}
try await StorageService.shared.batchInsert(entity: TrackPoint.entity(), batchInsertRequest: batchInsert, context: StorageService.shared.getBackgroundContext())
}
I also tried it without accessing the object from same context but instead tried directly using the Run object that i had created. It didn't crash, but it still didn't create the relationship.Also it forced me to remove the concurrencydebug run argument.
func addTrackPoints(run: RunModel, object: Run) async throws {
let locations = run.getLocations()
let count = run.getLocations().count
var index = 0
let batchInsert = NSBatchInsertRequest(entity: TrackPoint.entity()) { (managedObject: NSManagedObject) -> Bool in
guard index < count else { return true }
if let trackPoint = managedObject as? TrackPoint {
let data:CLLocation = locations[index]
trackPoint.run = object
}
index += 1
return false
}
try await StorageService.shared.batchInsert(entity: TrackPoint.entity(), batchInsertRequest: batchInsert, context: StorageService.shared.getBackgroundContext()) }
StorageService
public func batchInsert(entity: NSEntityDescription, batchInsertRequest: NSBatchInsertRequest, context: NSManagedObjectContext? = nil) async throws {
var taskContext:NSManagedObjectContext? = context
if(taskContext == nil) {
taskContext = StorageService.shared.newTaskContext()
// Add name and author to identify source of persistent history changes.
taskContext?.name = "importContext"
taskContext?.transactionAuthor = "import\(entity.name ?? "entity")"
}
/// - Tag: performAndWait
try await taskContext?.perform {
// Execute the batch insert.
do{
let fetchResult = try taskContext?.execute(batchInsertRequest)
if let batchInsertResult = fetchResult as? NSBatchInsertResult,
let success = batchInsertResult.result as? Bool, success {
return
}
} catch {
self.logger.error("Failed to execute batch insert request. \(error)")
}
throw SSError.batchInsertError
}
logger.info("Successfully inserted data for \(entity.name ?? "entity")")
}
Any help would be deeply appreciated :-)
How app works, I send request to server, get some results and want data to be saved in core data for further usage to send request to server only when needed. so next time I will query data from database.
Here is sample:
I always save data in background context, which is configured like this:
func getBgContext() -> NSManagedObjectContext {
let bgContext = self.persistenceController.container.newBackgroundContext()
bgContext.automaticallyMergesChangesFromParent = true
bgContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return bgContext
}
Next I construct my data models like this so decoder will handle entity creation and data parsing + insertion in dbContext:
public class SomeDataModel: NSManagedObject, Codable {
var entityName: String = "SomeDataModel"
enum CodingKeys: String, CodingKey {
case id = "id"
case someData = "someData"
}
public required convenience init(from decoder: Decoder) throws {
guard
let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: "SomeDataModel", in: context)
else {
throw DecoderConfigurationError.missingManagedObjectContext
}
self.init(entity: entity, insertInto: context)
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(Int32.self, forKey: .id)
someData = try values.decodeIfPresent(String.self, forKey: .someData)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(someData, forKey: .someData)
}
func toExternalModel() -> SomeExternalUsableModel {
return SomeExternalUsableModel(id: id, someData: someData)
}
}
extension SomeDataModel {
#nonobjc public class func fetchRequest() -> NSFetchRequest<SomeDataModel> {
return NSFetchRequest<SomeDataModel>(entityName: "SomeDataModel")
}
#NSManaged public var someData: String?
#NSManaged public var id: Int32
}
extension SomeDataModel: Identifiable {
}
to pass dbcontext to decoder I do next:
extension CodingUserInfoKey {
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
}
dbContext - create background context somewhere in API helper class, and use this context for all the parsings below.
and next I do parsing with decoder when respond from server comes:
let model = try self.dbContext.performAndWait {
let jsonDecoder = JSONDecoder()
let jsonEncoder = JSONEncoder()
// pass context to decoder/encoder
jsonDecoder.userInfo[CodingUserInfoKey.managedObjectContext] = self.dbContext
jsonEncoder.userInfo[CodingUserInfoKey.managedObjectContext] = self.dbContext
// parse model, used generic for reuse for other models
let model = try jsonDecoder.decode(T.self, from: result.data)
// after this line - all the data is parsed from response from server, and saved to dbContext, and contained in model as well
if self.dbContext.hasChanges {
do {
try self.dbContext.save()
self.dbContext.refreshAllObjects() // refresh context objects to ELIMINATE all outdated db objects in memory (specially when you will have relations, they could remain in memory until updated)
} catch let error {
// process error
}
}
return model
}
// do with saved and ready to use data in models whatever needed:
return model
and extensions used for performAndWait
extension NSManagedObjectContext {
func performAndWait<T>(_ block: () throws -> T) throws -> T? {
var result: Result<T, Error>?
performAndWait {
result = Result { try block() }
}
return try result?.get()
}
func performAndWait<T>(_ block: () -> T) -> T? {
var result: T?
performAndWait {
result = block()
}
return result
}
}

I am trying to pass my user object in documents.map( )to make a array its decodable and wont let me pass it without conforming to decodable protocol

func fetchUsers(forConfig config: ExplorerViewModelConfiguration) {
COLLECTION_USERS.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
let usersArray = documents.map({ User(from: <#T##Decoder#>) })
//The line above is where I keep getting the error while I'm trying to
// make an array out of my user object to map the message data to
switch config {
case .search:
self.users = usersArray
case .newMessage:
self.users = usersArray.filter({ !$0.isCurrentUser})
}
}
}

Combine these into a single var

Apologies for the stupid question, I'm still really new to the Swift language.
Following up on this answer by #matt, I want to combine these two statements into a single var
UserDefaults.standard.set(try? PropertyListEncoder().encode(songs), forKey:"songs")
if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
let songs2 = try? PropertyListDecoder().decode(Array<Song>.self, from: data)
}
I've thought maybe using a var with didSet {} like something along the lines of
var test: Array = UserDefaults.standard. { // ??
didSet {
UserDefaults.standard.set(try? PropertyListEncoder().encode(test), forKey: "songs")
}
}
But I can't think of where to go from here.
Thanks for the help in advance :))
The property should not be a stored property. It should be a computed property, with get and set accessors:
var songsFromUserDefaults: [Song]? {
get {
if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
return try? PropertyListDecoder().decode(Array<Song>.self, from: data)
} else {
return nil
}
}
set {
if let val = newValue {
UserDefaults.standard.set(try? PropertyListEncoder().encode(val), forKey:"songs")
}
}
}
Notice that since the decoding can fail, the getter returns an optional. This forces the setter to accept an optional newValue, and I have decided to only update UserDefaults when the value is not nil. Another design is to use try! when decoding, or return an empty array when the decoding fails. This way the type of the property can be non-optional, and the nil-check in the setter can be removed.
While you can use computed properties like Sweeper suggested (+1), I might consider putting this logic in a property wrapper.
In SwiftUI you can use AppStorage. Or you can roll your own. Here is a simplified example:
#propertyWrapper public struct Saved<Value: Codable> {
private let key: String
public var wrappedValue: Value? {
get {
guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
return (try? JSONDecoder().decode(Value.self, from: data))
}
set {
guard
let value = newValue,
let data = try? JSONEncoder().encode(value)
else {
UserDefaults.standard.removeObject(forKey: key)
return
}
UserDefaults.standard.set(data, forKey: key)
}
}
init(key: String) {
self.key = key
}
}
And then you can do things like:
#Saved(key: "username") var username: String?
Or
#Saved(key: "songs") var songs: [Song]?

Save state in Userdefaults

I have a class that saves the state of something, in my case some variable of the ViewController, but sometimes it loads wrong or old data, but I can't figure out why.
Maybe somebody can have a look of my code and see if it makes sense.
class TopFlopState: Codable, PersistenceState {
var group: Groups = .large {
didSet {
save()
}
}
var base: Bases = .usd {
didSet {
save()
}
}
var valueOne: StatIntervalBaseModel = StatIntervalBaseModel(stat: "ppc", interval: "24h", base: "usd") {
didSet {
save()
}
}
init(){
let savedValues = load()
if savedValues != nil {
self.group = savedValues!.group
self.base = savedValues!.base
self.valueOne = savedValues!.valueOne
}
}
}
This is the PersistenceState protocol:
/**
Saves and Loads the class, enum etc. with UserDefaults.
Has to conform to Codable.
Uses as Key, the name of the class, enum etc.
*/
protocol PersistenceState {
}
extension PersistenceState where Self: Codable {
private var keyUserDefaults: String {
return String(describing: self)
}
func save() {
saveUserDefaults(withKey: keyUserDefaults, myType: self)
}
func load() -> Self? {
return loadUserDefaults(withKey: keyUserDefaults)
}
private func saveUserDefaults<T: Codable>(withKey key: String, myType: T){
do {
let data = try PropertyListEncoder().encode(myType)
UserDefaults.standard.set(data, forKey: key)
print("Saved for Key:", key)
} catch {
print("Save Failed")
}
}
private func loadUserDefaults<T: Codable>(withKey key: String) -> T? {
guard let data = UserDefaults.standard.object(forKey: key) as? Data else { return nil }
do {
let decoded = try PropertyListDecoder().decode(T.self, from: data)
return decoded
} catch {
print("Decoding failed for key", key)
return nil
}
}
}
If a value gets set to the value it should automatically save, but like I set sometimes it saves the right values but loads the wrong ones...
In my opinion, It return the cache. Because in Apple official documentation, it state
UserDefaults caches the information to avoid having to open the user’s defaults database each time you need a default value
Maybe you can change the flow, when to save the data. In your code show that you call save() 3 times in init().

Unable to get computed propery working with NSManagedObject

I would like to subclass from NSManagedObject since I have lots of fields in my entity.
At the beginning I've created this:
class RecordModel: NSManagedObject {
// MARK: - Variables
class var coreDataEntityName: String {
return "Record"
}
/// Cover image of the song. Optional.
var coverImage: UIImage? {
set (image) {
if image != nil {
if let coverImageData = UIImagePNGRepresentation(coverImage!) {
setValue(coverImageData, forKeyPath: "coverImageData")
}
}
}
get {
let coverImageData = value(forKeyPath: "coverImageData") as? Data
if coverImageData != nil {
return UIImage(data: coverImageData!)
}
return nil
}
}
/**
Initializes record with image object.
*/
init(image: UIImage?) {
let context = CoreDataManager.shared.managedContext
let entity = NSEntityDescription.entity(forEntityName: RecordModel.coreDataEntityName, in: context)!
super.init(entity: entity, insertInto: context)
self.coverImage = image
}
}
And I get EXT_BAD_ACCESS error on self.coverImage = image.
What I wanted to achieve with my coverImage property is to cover value and setValue methods with it for more convenience.
If I step in debugger onto that line and perform
e setValue(UIImagePNGRepresentation(image!), forKeyPath: "coverImageData")
which is basically what my custom setter does, I get no runtime errors.
It looks like I completely don't understand computed properties. Please help.
EDIT:
I've tried this as well:
var coverImage: UIImage? {
set (image) {
if image != nil {
coverImageData = UIImagePNGRepresentation(image!)
}
}
get {
if coverImageData != nil {
return UIImage(data: coverImageData!)
}
return nil
}
}
#NSManaged var coverImageData: Data?
Get code 2 now. Looks like an infinite loop is being executed for some reason.