I am working on a swift project, here is what I am trying to do:
I have a service class, responsible for saving data to firestore (bulk insert or single insert).
The service is used in a couple of viewControllers.
When using my dev schema I would like the app not writing anything to Firestore.
At the moment I have a env variable that act as a flag and in each function in my service I need to check if whether is set or not for saving data
func singleInsert(collection: String, data: [String: Any], id: String?) {
if !isLoggingEnabled {
// just print some stuff
return
}
// save my data in firestore
}
It works.. but it is really ugly, I was wondering if there is a better way to do it. It is worth notice that I want to disable firestore only within my service class. There are instances in the app (which don't use the service) where firestore need to be always enabled.
I end up using something close to a factory pattern.
class RealTimeEventFactory {
var realTimeEventServiceImpl: RealTimeEventProtocol
init(isLoggingEnabled: Bool, errorHandler: ErrorHandlerProtocol) {
if isLoggingEnabled {
self.realTimeEventServiceImpl = RealTimeEventService(errorHandler: errorHandler)
} else {
self.realTimeEventServiceImpl = RealTimeEventServiceMock()
}
}
}
So I have two services both conforming to same protocol. Based on the isLoggingEnabled flag the factory will generate the required instance.
It's not perfect but seems to work fine
Related
This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed last year.
I am building a mobile app with swift, and am having some syntax issues as I am not a developer. The structure and logic of the application is really rough and surely incorrect, however we just need something that functions. (It is a school project and my team got no devs).
Anyways, we have a MySQL database that will be used as a middleman between our badge server/admin app, and our mobile app. Currently when you go to https://gatekeeperapp.org/service.php , you will see the current database data, taken by a php script and hosted there as JSON. Currently in Swift I have a struct with a function that takes this JSON data, and maps it to variables. The idea is to then pass these pulled variables into a separate set of functions that will check the pulled long/lat against the mobile devices location, and then return whether they match or not. This value would be updated, re-encoded to JSON, and pushed to a web service that would go about changing the values in the database so the badge server could use them.
Where I am currently I can see that values are being pulled and mapped and I can set a variable in a separate function to the pulled value, but then I can only seem to output this value internally, rather than actually use it in the function. I get a type error saying that the pulled values are of type (). How can I properly use these values? Ultimately I think I would want to convert the () to a double, so I could properly compare it to the Long/Lat of the device, and then will need to re-encode the new values to JSON.
Swift Code -- struct function
Swift code -- JSON struct
Swift code -- using pulled data
Your closure is called asynchronously, which means that the outer function where you are expecting to use the values has already returned by the time the closure is called. Instead, you probably need to call some other function from the closure, passing the values you've received.
class MyClass {
func fetchUserData() {
UserData().fetchUser { [weak self] user, error in
DispatchQueue.main.async {
if let user = user {
self?.handleSuccess(userID: user)
} else if let error = error {
self?.handleError(error)
}
}
}
}
private func handleSuccess(userID: String) {
print(userID)
// Do something with userID. Maybe assign it to a property on the class?
}
private func handleError(_ error: Error) {
print(error)
// Handle the error. Maybe show an alert?
}
}
My project consist of obj-c and swift classes. I use Firebase 7.3.0.
I manually log screen_view event for my screens. I call this method in viewWillAppear or viewDidAppear like this:
#objc class MyAnalyticConstants: NSObject {
static let myScreenName = "AwesomeScreen"
}
class MyViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
MyAnalyticsClass().logScreenViewEvent(name: MyAnalyticConstants.myScreenName)
}
}
class MyAnalyticsClass {
func logScreenViewEvent(name: String, parameters: [String: Any]? = nil) {
var param = [String: Any]()
if let parameters = parameters {
param = parameters
}
param[AnalyticsParameterScreenName] = name
logEvent(name: AnalyticsEventScreenView, parameters: param)
}
func logEvent(name: String, parameters: [String: Any]?) {
analytics.logEvent(name, parameters: parameters)
}
}
I turned off automatic screenview reporting by setting FirebaseAutomaticScreenReportingEnabled to NO (Boolean) in the Info.plist. I use struct with static names for my screens.
However, sometimes I see (not set) value for "screen_view" event inside my google analytics path exploration for production. I can't catch this while using DebugView.
screenshot
I would really appreciate it, if somebody could help me to fix or explain it.
EDIT:
I swizzled firebase method
+ (void)xxx_logEventWithName:(NSString *)name
parameters:(nullable NSDictionary<NSString *, id> *)parameters {
[self xxx_logEventWithName:name parameters:parameters];
if ([name isEqualToString:#"screen_view"] && [parameters[#"screen_name"] length] <= 3) {
NSLog(#"%#", #[][1]);
}
and jumped through app during 30 min. I didn't catch up crash. Any other ideas?
Ok, there are a few things you could typically do in this case.
I wrote it as an afterthought, but it's something you should make sure of before doing technical debugging that follows: you should go to your analytics property/view and debug the filters. Maybe you have replacing filters interfering with your values, but I presume you checked your data in a full and unfiltered view where your app is the sole "stream" of data. This is important. The bug can come from a different app to this property, so either make sure you're the only source, or make sure you filter out other sources/apps/platforms.
Check your logEvent function calls where you either send AnalyticsEventScreenView or just the "screen_view" string as the first parameter. You see how the Firebase lib uses one function to send all kinds of events? They now treat screenviews as events. Which has its elegancy, but also may lead to unintended mistakes. Check what the globals actually mean in here: https://github.com/firebase/firebase-cpp-sdk/blob/0c8c8b29bc2d62d66c6ac49ff2c3fb04f815a687/analytics/ios_headers/FIREventNames.h
Check your logScreenViewEvent function calls. Pay attention to cases when you pass the first parameter as a variable. Also make sure you're never setting the AnalyticsParameterScreenName, which is also known as a string "screen_name" from here: https://github.com/firebase/firebase-cpp-sdk/blob/0c8c8b29bc2d62d66c6ac49ff2c3fb04f815a687/analytics/ios_headers/FIRParameterNames.h in the parameters dictionary, the second attribute. Cuz setting it there will effectively overwrite whatever is the first attribute you're setting. I actually usually suggest having only one argument for the screenview function declaration, especially to avoid collisions like this.
Oh, almost forgot. Make sure you ALWAYS use your neat MyAnalyticsClass wrapper and never call the native logEvent(). I would just check all files where you include the Firebase sdk and see if it should be replaces with the wrapper.
Finally, if the above didn't help, you can insert the check in both your function wrappers to throw an error whenever the event name equals to "screen_view" and the "screen_name" parameter's length not more than 2 character (I'm just trying to include all falsy values, so null, undefined, nil, whatever). And run unit tests or even better - regression testing with things set like that. Well, or manually test it out, watching for the errors in the console rather than the web debugger.
I need to observe changes of an Entity after import occurred.
Currently I have next logic:
Save Entity with temp identifier (NSManagedObject.objectId) to local core data storage.
Send Entity to the server via Alamofire POST request.
Server generates JSON and reply with the almost the same Entity details but with modified identifier which was NSManagedObject.objectId previously. So the local one Entity id will be updated with server id.
Now when I received new JSON I do transaction.importUniqueObjects.
At this step I want to inform my datasource about changes. And refetch data with updated identifiers.
So my DataSource has some Entities in an array, and while I use this datasource to show data it's still static information in that array which I fetched before, but as you see on the step number 4 I already updated core data storage via CoreStore import and want DataSource's array to be updated too.
I found some information regarding ListMonitor in CoreStore and tried to use it. As I can see this method works when update comes
func listMonitorDidChange(_ monitor: ListMonitor)
but I try to refetch data somehow. Looks like monitor already contains some most up to date info.
but when I do this:
func listMonitorDidChange(_ monitor: ListMonitor<MyEntity>) {
let entities = try? CoreStore.fetchAll(
From<MyEntity>()
.orderBy(.ascending(\.name))
) // THERE IS STILL old information in database, but monitor instance shows new info.
}
And then code became like this:
func listMonitorDidChange(_ monitor: ListMonitor<MyEntity>) {
var myEntitiesFromMonitor = [MyEntity]()
for index in 0...monitor.numberOfObjects() {
myEntitiesFromMonitor.append(monitor[index])
}
if myEntitiesFromMonitor.count > 0 {
// HERE we update DataSource
updateData(with: myEntitiesFromMonitor)
}
}
not sure if I am on the right way.
Please correct me if I am wrong:
As I understood each time core data gets updated with new changes, monitor gets updated as well. I have not dive deep into it how this was made, via some CoreData context notification or whatever but after you do something via CoreStore transaction, such as create or update or delete object or whatever you want, monitor gets update. Also it has callback functions that you need to implement in your class where you want to observe any changes with data model:
Your classes such as datasource or some service or even some view controller (if you don't use any MVVP or VIPER or other design patterns) need to conform to ListObserver protocol in case you want to listen not to just one object.
here are that functions:
func listMonitorDidChange(monitor: ListMonitor<MyPersonEntity>) {
// Here I reload my tableview and this monitor already has all needed info about sections and rows depend how you setup monitor.
// So you classVariableMonitor which I provide below already has up to date state after any changes with data.
}
func listMonitorDidRefetch(monitor: ListMonitor<MyPersonEntity>) {
// Not sure for which purposes it. I have not received this call yet
}
typealias ListEntityType = ExerciseEntity
let classVariableMonitor = CoreStore.monitorSectionedList(
From<ListEntityType>()
.sectionBy(#keyPath(ListEntityType.muscle.name)) { (sectionName) -> String? in
"\(String(describing: sectionName)) years old"
}
.orderBy(.ascending(\.name))
.where(
format: "%K == %#",
#keyPath(ListEntityType.name),
"Search string")
)
All other thing documented here so you can find info how to extract info from monitor in your tableview datasource function.
Thanks #MartinM for suggestion!
My app uses a custom class as its data model:
class Drug: NSObject, NSCoding {
// Properties, methods etc...
}
I have just created a Today extension and need to access the user’s data from it, so I use NSCoding to persist my data in both the app container and the shared container. These are the save and load functions in the main app:
func saveDrugs() {
// Save to app container
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.ArchiveURL.path)
if isSuccessfulSave {
print("Drugs successfully saved locally")
} else {
print("Error saving drugs locally")
}
// Save to shared container for extension
let isSuccessfulSaveToSharedContainer = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.SharedArchiveURL.path)
if isSuccessfulSaveToSharedContainer {
print("Drugs successfully saved to shared container")
} else {
print("Error saving drugs to shared container")
}
}
func loadDrugs() -> [Drug]? {
return NSKeyedUnarchiver.unarchiveObject(withFile: Drug.ArchiveURL.path) as? [Drug]
}
I encountered the problem of class namespacing where the NSKeyedUnarchiver in my Today extension could not decode the object properly, so I used this answer and added #objc before the class definition:
#objc(Drug)
class Drug: NSObject, NSCoding {
// Properties, methods etc...
}
This solved the problem perfectly. However, this will be version 1.3 of my app, and it seems this breaks the unarchiving process for pre-existing data (as I thought it might).
What is the best way to handle this scenario, as if I just make this change, the new version of the app will crash for existing users!
I cannot find any other answers about this, and I am not sure that the NSKeyedArchiver.setClass() method is relevant, nor am I sure where to use it.
Any help would be gratefully received. Thanks.
This is exactly the use case for NSKeyedUnarchiver.setClass(_:forClassName:) — you can migrate old classes forward by associating the current classes for their old names:
let unarchiver = NSKeyedUnarchiver(...)
unarchiver.setClass(Drug.self, forClassName: "myApp.Drug")
// unarchive as appropriate
Alternatively, you can provide a delegate conforming to NSKeyedUnarchiverDelegate and providing a unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:), but that's likely overkill for this scenario.
Keep in mind that this works for migrating old data forward. If you have newer versions of the app that need to send data to old versions of the app, what you'll need to do is similar on the archive side — keep encoding the new class with the old name with NSKeyedArchiver.setClassName(_:for:):
let archiver = NSKeyedArchiver(...)
archiver.setClassName("myApp.Drug", for: Drug.self)
// archive as appropriate
If you have this backwards compatibility issue, then unfortunately, it's likely you'll need to keep using the old name as long as there are users of the old version of the app.
I would like to have synchronized http request results with my Core Data database. For example very basic class:
class Category: NSManagedObject {
#NSManaged var id: NSNumber
#NSManaged var name: String
#NSManaged var imageUrl: String
}
And I have this method for getting results from request:
apiManager.getRequestJsonParse(Constants.Server.BackendUrl + "categories?lang=cs", completion: { (result, error) -> Void in
completion(categories: [], error: error)
})
Result is dictionary parsed from json. It's okay but now I want to parse dictionary to my class, check if exists in database, if it exists update properties and if not than add new category to database.
My approach that I was using in apps before was that I have two classes: Category and CategoryCD (CD like Core Data) and first I parse json to Category class than for all categories I check if any CategoryCD (saved in CD) has same Id and then update, or add or other things.
Now I am thinking if there is better way how can I do it. I could each time when I download new results delete my database for this class and then add all results. The problem with this way is that what if I have something for some classes that I want keep. Like if I download and save images then I would rather still have connection between class and saved image.
I was thinking about my old approach but reduce 2 almost same classes (1 for Core Data and 1 same but for parsing) to 1 Core Data class. But then there is a problem when I init this class I must always create in database right? So It could be complicated.
What are you using for this? I think it's very common problem. You have list of items and I would like to have them available offline (with all data I downloaded before) but than when I connect to internet I would like to update with new results (not download all from server, just responses which I requested).
It is a very common problem and it has been solved hundreds of ways. BTW, This is not "synchronizing" it is "caching" for offline use.
Do not create two objects as that is redundant. Create just the Core Data objects as part of the parse.
A standard JSON parse would look like this:
Convert to Objects using NSJSONSerializer.
Fetch all unique IDs from the JSON objects using KVO
Fetch all existing objects from Core Data based on uniqueIDs
Insert all objects that do not currently exist
If you are going to update objects then #4 would do both.
Search on Stackoverflow and you will find plenty of examples of how to do this.
I do something similar in one of my apps.
Here is some generic code to fetch an item and update it. If it doesn't exist, it creates it.
func insertOrUpdateManagedObject(id: Int16, name: String) -> YourManagedObject? {
var managedObject: YourManagedObject?
if let context = self.managedObjectContext {
if let fetchResult = fetchManagedObjectWithId(id) {
managedObject = fetchResult
managedObject?.name = name
}
else {
managedPhrase = NSEntityDescription.insertNewObjectForEntityForName("YourManagedObject", inManagedObjectContext: context) as? YourManagedObject
managedObject?.id = id
managedObject?.name = name
}
}
println("Created a managed object \(managedObject)")
return managedObject
}