So right now I'm working on sending the match data in a turn based game and I was using this post as a reference.
Good practices for Game Center matchData
I created a new class and it implements NSCoding. It currently only holds one variable for a NSString. This is the code for when I send the match data.
self.game.status = #"Test";
NSData *updatedMatchData = [NSKeyedArchiver archivedDataWithRootObject:self.game];
[self.currentMatch endTurnWithNextParticipants:[NSArray arrayWithObject:nextPerson]
turnTimeout:1000
matchData:updatedMatchData
completionHandler:^(NSError *error) {
if (error) {
NSLog(#"Error: %#", error);
}
}];
NSLog(#"Successfully ended turn");
}
When I try retrieving the match data, I tried this.
[match loadMatchDataWithCompletionHandler:^(NSData *matchData, NSError *error) {
if (matchData)
{
RaceGame *updatedGame = [NSKeyedUnarchiver unarchiveObjectWithData:matchData];
NSLog(#"Match Data: %#", updatedGame.status); //prints null
callback(matchData);
}
}];
However, status is null. I've checked that match isn't null either. I also printed out the match and it said that matchData.length = 135, but I kept changing things around and it was still 135 so I'm not sure if that's helpful.
Any ideas on why status isn't changing?
--EDIT--
.m
#implementation RaceGame
#synthesize status;
#pragma mark - NSCoding protocol
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:status forKey:#"status"];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init]) {
self.status = [aDecoder decodeObjectForKey:#"status"];
}
return self;
}
#end
.h
#interface RaceGame : NSObject <NSCoding> {
NSString *status;
}
/* Match Data */
#property (nonatomic, copy) NSString *status;
#end
Never mind, really stupid mistake by me. I was testing it on two devices and I only ran the updated version on one of the devices.
Related
My app currently uses this deprecated function:
id unarchivedObject=[NSKeyedUnarchiver unarchiveObjectWithData:codedData];
if([unarchivedObject isKindOfClass:[NSDictionary class]]){
// currently returns TRUE when reading existing user data.
}
To update, I've converted to this:
id unarchivedObject=[NSKeyedUnarchiver unarchivedObjectOfClass:[NSDictionary class] fromData:codedData error:nil];
if([unarchivedObject isKindOfClass:[NSDictionary class]]){
// currently returns FALSE when reading existing user data.
}
The data was originally encoded like this:
-(void)encodeWithCoder:(NSCoder*)encoder{
[encoder encodeObject:text forKey:#"text"];
}
-(instancetype)initWithCoder:(NSCoder*)decoder{
if(self=[super init]){
text=[decoder decodeObjectForKey:#"text"];
}
What could be causing the IF statement to return FALSE using the newer code?
Please note that I am concerned primarily with reading existing data stored prior to deprecating the Archiving functions. Simply changing to the newer functions does not resolve the issue.
Interesting question! I've been supporting iOS 10.0 so I haven't encountered such issue until I saw this. I was tinkering for an hour and I successfully found the issue.
What could be causing the IF statement to return FALSE using the newer
code?
It's because your unarchivedObject object is nil!
If you use the parameter error in the new method, you would see an error like this:
Error Domain=NSCocoaErrorDomain Code=4864 "This decoder will only
decode classes that adopt NSSecureCoding. Class 'QTPerson' does not
adopt it." UserInfo={NSDebugDescription=This decoder will only decode
classes that adopt NSSecureCoding. Class 'QTPerson' does not adopt it.
But how do we get the correct value for this unarchivedObject and not nil? It would take a couple of steps.
First off, make your model/class conform to <NSCoding, NSSecureCoding>
Example:
QTPerson.h
#import <Foundation/Foundation.h>
#class QTPerson;
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Object interfaces
#interface QTPerson : NSObject <NSCoding, NSSecureCoding>
#property (nonatomic, copy) NSString *text;
#end
NS_ASSUME_NONNULL_END
And then implement the protocol methods:
QTPerson.m
#import "QTPerson.h"
#implementation QTPerson
+ (BOOL)supportsSecureCoding {
return YES;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:_text forKey:#"text"];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
_text = [coder decodeObjectOfClass:[NSString class] forKey:#"text"];
}
return self;
}
#end
And then when archiving an object, you would want to pass YES to the parameter requiringSecureCoding, like so:
QTPerson *person = [[QTPerson alloc] init];
person.text = #"Glenn";
NSData *codedData1 = [NSKeyedArchiver archivedDataWithRootObject:person requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setValue:codedData1 forKey:#"boom"];
Lastly, when unarchiving, just do what you did correctly, like so:
NSData *codedData = [[NSUserDefaults standardUserDefaults] dataForKey:#"boom"];
NSError *er;
id unarchivedObject=[NSKeyedUnarchiver unarchivedObjectOfClass:[QTPerson class] fromData:codedData error:&er];
if([unarchivedObject isKindOfClass:[QTPerson class]]){
NSLog(#"TRUE!");
} else {
NSLog(#"FALSE!");
}
Voila! You'll get nonnull object unarchivedObject, hence the TRUE/YES value you're looking for!
I have an application that works with the server.
I would like to make an offline mode that is when a user creates something or changes it was saved somewhere and then when an internet connection appears - these requests go to the server. How can I achieve this? How to store blocks in NSUserDefaults? How not to lose pointers to these blocks?
It can be different controllers. Help me please. I apologize for my bad English.
I've tried to save this objects in NSUserDefaults
typedef void (^ExecutionBlock)(void);
#interface OfflineBlockObject : NSObject
#property (nonatomic, copy) ExecutionBlock block;
#property (nonatomic, strong) NSArray<NSMutableArray *> *operandsArray;
#property (nonatomic, strong) NSArray *conditionsArray;
#end
OfflineBlockObject *blockObject = [[OfflineBlockObject alloc] init];
BOOL first = [self.reservation.reservationID boolValue];
NSArray *conditions = #[#(first), #(self.shouldCallSetTagsForReservation)];
NSArray *operands = #[#[[self.reservation json], self.reservation.reservationID ? : #0, self.selectedTags ? : #[]],
#[[self.reservation json], #(self.shouldForceApproved), self.selectedTags? : #[]]];
blockObject.conditionsArray = [conditions copy];
blockObject.operandsArray = [operands copy];
#weakify(blockObject);
blockObject.block = [^{
#strongify(blockObject);
if ([blockObject.conditionsArray[0] boolValue]) {
ReservationsModel *m = [[ReservationsModel alloc] init];
[m editReservation:blockObject.operandsArray[0][0] success:^(id responseObject) {
if ([blockObject.conditionsArray[1] boolValue]) {
[m setReservationTags:blockObject.operandsArray[0][1] tags:blockObject.operandsArray[0][2] success:nil failure:nil];
}
} failure:nil];
} else {
self.reservation.eventId = self.reservationEvent.eventInfoID;
ReservationsModel *m = [[ReservationsModel alloc] init];
[m createReservation:blockObject.operandsArray[1][0] shouldForceApproved:[blockObject.operandsArray[1][1] boolValue] success:^(id responseObject) {
Reservation *reservation = [Reservation reservationWithJson:responseObject];
if (reservation.reservationID) {
if ([blockObject.conditionsArray[1] boolValue]) {
[m setReservationTags:reservation.reservationID tags:blockObject.operandsArray[0][2] success:nil failure:nil];
}
}
} failure:nil];
}
} copy];
[[OfflineQueueHelper sharedHelper] addTask:blockObject];
But my block property becomes null :(
So I have a pod that does pretty much this, but it's in Swift (the Obj-C made the interface too convoluted) - https://cocoapods.org/pods/OfflineRequestManager. You can make any object that conforms to OfflineRequest that wraps whatever network request you want to make it to the server. You can also provide a dictionary that gets written to disk if you want to make sure that it persists through app termination. We've been using it in our internal apps for a while now, so hopefully somebody else can get some use out of it.
The simplest use case would look something like the following, though most actual cases (saving to disk, specific request data, etc.) will have a few more hoops to jump through:
import OfflineRequestManager
class SimpleRequest: OfflineRequest {
func perform(completion: #escaping (Error?) -> Void) {
doMyNetworkRequest(withCompletion: { response, error in
handleResponse(response)
completion(error)
})
}
}
///////
OfflineRequestManager.defaultManager(queueRequest: SimpleRequest())
I have a class with an object incall. I have a method that sets it and another methods that runs a method available for that object.
Here is my header file:
#interface RCTPlivo : NSObject <PlivoEndpointDelegate, CXProviderDelegate>
#property (nonatomic) PlivoIncoming *incall;
#property (nonatomic) PlivoEndpoint *endpoint;
#end
And here is my implementation file:
#implementation RCTPlivo
- (void)login {
endpoint = [[PlivoEndpoint alloc] init];
[endpoint login:plivoUser AndPassword:plivoPass];
endpoint.delegate = self;
}
- (void)triggerIncomingCall {
...
CXProvider *callkitProvider = [[CXProvider alloc] initWithConfiguration: configuration];
[callkitProvider setDelegate:self queue:nil];
...
[callkitProvider reportNewIncomingCallWithUUID:currentCall update:update completion:^(NSError * _Nullable error) {
if (error) {
NSLog(#"Error: %#", error);
}
}];
}
- (void)onIncomingCall:(PlivoIncoming *)incoming {
// setting
self.incall = incoming
}
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action
{
// Here self.incall is null
[self.incall answer];
}
#end
When i log self.incall in perfromAnswerCall delegate it's null. When I log it in the onIncomingCall delegate the variable is set.
What am I missing here?
Update
Added the code that initializes the delegates and removed ivars.
Your interface should be:
#interface RCTPlivo : NSObject <PlivoEndpointDelegate>
#property (nonatomic, strong) PlivoIncoming *incall;
#end
and your implementation should be:
#implementation RCTPlivo
- (void)onIncomingCall:(PlivoIncoming *)incoming {
self.incall = incoming;
}
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
[self.incall answer];
}
#end
One way this can happen is that you somehow have two separate instances of RCTPlivo. Try stopping the debugger in each of those calls and in the debugger do:
(lldb) po self
If everything's ok then the addresses should be the same.
The infall property is not defined as strong. So we can assume that there is no strong reference to the original object outside this method and it was released.
Update
This property was mentioned as delegate so its weak nature can be a designated behaviour and if this is an option the message sender should have a strong property holding the object reference.
I have a managed object named SpecialItem and call setSubcategory to change the subcategory. When I save the temporary context and merge with the main context, somehow setSubcategory is called passing in nil as the subcategory. This often results in the SpecialItem object being saved with myProperty set to nil. I don't know what is calling setSubcategory. I'm not explicitly calling setSubcategory:nil.
My question is, what is happening and how can I fix this?
This is the managed object implementation:
#interface SpecialItem : GenericItem
#property (nonatomic, strong) Subcategory *subcategory;
#property (nonatomic, strong) MyProperty *myProperty;
#end
#implementation SpecialItem
#dynamic subcategory;
#dynamic myProperty;
- (void)setSubcategory:(Subcategory *)subcategory
{
[self willChangeValueForKey:#"subcategory"];
[self willChangeValueForKey:#"myProperty"];
[self setPrimitiveValue:subcategory forKey:#"subcategory"];
[self setPrimitiveValue:subcategory.category.myProperty forKey:#"myProperty"];
[self didChangeValueForKey:#"myProperty"];
[self didChangeValueForKey:#"subcategory"];
}
// ...
The managed object contexts are setup like so:
self.tempContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.tempContext.parentContext = self.dataManager.mainContext;
Eventually I have this:
[self saveTempContext];
Here is the saveContext implementation:
- (void)saveContext
{
LogAndPrint(#"Before save.");
[self.tempContext performBlockAndWait:^{
NSError *error = nil;
if (![self.tempContext save:&error])
{
LogAndPrint(#"Error occurred while saving context: %#", error);
}
}];
LogAndPrint(#"Middle of save.");
[self.dataManager.mainContext performBlockAndWait:^{
NSError *error = nil;
if (![self.dataManager.mainContext save:&error])
{
LogAndPrint(#"Error occurred while saving context: %#", error);
}
}];
[self.dataManager synchronize];
LogAndPrint(#"After save.");
}
Here is the synchronize implementation:
- (void)synchronize
{
LogAndPrint(#"Synchronizing Core Data changes.");
if (self.persistentContext.hasChanges) {
[self.persistentContext performBlockAndWait:^{
NSError *error = nil;
if (![self.persistentContext save:&error]) {
LogAndPrint(#"Error occurred while saving persistent context: %#", error);
}
}];
}
}
I was never able to figure out why this was happening. But I did find a solution that works. I changed the code that was calling setSubcategory to call a new method named [SpecialItem updateSubcategory:subcategory].
- (void)updateSubcategory:(Subcategory *)subcategory
{
self.subcategory = subcategory;
self.myProperty = subcategory.category.myProperty;
}
This fixed it and the code has been working fine for several months now.
I am having a lot of trouble wrapping my head around the best way to use blocks. I am trying to retrieve pedometer data, and the method of accessing the data is a block...
[self.pedometer queryPedometerDataFromDate:yesterday
toDate:midnightOfToday
withHandler:^(CMPedometerData *pedometerData, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(#"Pedometer is NOT available.");
}
else {
NSLog(#"Steps %#", pedometerData.numberOfSteps);
yesterdaysNumbersLabel.text = [pedometerData.numberOfSteps stringValue];
[pedometerDictionary setValue:[pedometerData.numberOfSteps stringValue] forKey:#"2"];
}
});
}];
Using the above code I am able to get the data, log the data, and update the label on the screen, But I can't figure out how to set the data into an array or dictionary so I can do something else with it.
I understand why the arrays and dictionaries are always null... the blocks are running on a different thread and I am accessing them before the blocks have completed.
Can someone help me get through my head how to do something more with the data.
Update 1:
Right now I have this in .h
#property (strong, atomic) NSMutableDictionary *pedometerDictionary;
and I am synthesizing it in .m and I call this...
[self getNumbersForYesterday];
NSLog(#"Dictionary: %#", pedometerDictionary);
...which runs the above function and immediately tries to log the result. And like I said, I understand all the reasons it is NOT working. I just need to figure out how to change what i am doing to get it working.
Update 2:
This is in .h
#property (strong, atomic) NSMutableDictionary *pedometerDictionary;
and this is in .m
#synthesize pedometerDictionary;
- (id)init {
self = [super init];
if (self != nil) {
self.pedometerDictionary = [[NSMutableDictionary alloc] init];
}
return self;
}
and I am using it like this.
[self getNumbersForYesterday];
NSLog(#"Dictionary: %#", self.pedometerDictionary);
to call this.
- (void)getNumbersForYesterday {
[self.pedometer queryPedometerDataFromDate:yesterday
toDate:midnightOfToday
withHandler:^(CMPedometerData *pedometerData, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(#"Pedometer is NOT available.");
}
else {
NSLog(#"Steps %#", pedometerData.numberOfSteps);
yesterdaysNumbersLabel.text = [pedometerData.numberOfSteps stringValue];
[self.pedometerDictionary setValue:[pedometerData.numberOfSteps stringValue] forKey:#"2"];
}
});
}];
}
If I just wanted to keep all the work in the block I would be fine. What I have come to understand is that since blocks are asynchronous, I am trying to NSLog my dictionary, and the block isn't finished running yet. So, my dictionary is still NULL.
Dollars to donuts, your pedometerDictionary was never created in the first place (or it was, but the declaration isn't in a useful spot).
I.e. where is your line of code that says pedometerDictionary = [[NSMutableDictionary alloc] init];? And where is pedometerDictionary declared? How did you try to NSLog() values from it?
Also, use setObject:forKey:.
It is also odd that it is named pedometerDictionary. That is evidence that it is either declared as a global (which it shouldn't be), a local variable of whatever method contains the above code (which won't work), or you are declaring and using an instance variable directly.
The issue you are having is not a block timing issue, your dictionary should never be nil at worst it would contain no values.
You need to create your dictionary before using it. The appropriate place would be init method for most objects. If you are creating your object in Interface Builder then the method should be awakeFromNib.
To do something with the dictionary you can use an NSTimer or call a method from queryPedometerDataFromDate block handler. The use of #synchronized() directive is an example of how to keep access to the dictionary from overlapping at the same time in a threaded environment. This is not the case in this particular example as you are dispatching on the main thread and NSTimer also runs on the main thread. But should you go threaded #synchronized() would keep you from overlapping access.
#interface HelloWorld : NSObject
#property (retain, atomic) NSMutableDictionary *pedometerDictionary;
#property (retain, nonatomic) NSTimer *timer;
#end
#implementation HelloWorld
#synthesize pedometerDictionary, timer;
...
- (id)init {
self = [super init];
if (self != nil) {
self.pedometerDictionary = [NSMutableDictionary dictionary];
self.timer = [NSTimer timerWithTimeInterval:5.0 target:self selector:#selector(doSomethingInterestingWithDictionary:) userInfo:nil repeats:YES];
}
return self;
}
or
- (void)awakeFromNib {
self.pedometerDictionary = [NSMutableDictionary dictionary];
self.timer = [NSTimer timerWithTimeInterval:5.0 target:self selector:#selector(doSomethingInterestingWithDictionary:) userInfo:nil repeats:YES];
}
...
- (void)getNumbersForYesterday {
[self.pedometer queryPedometerDataFromDate:yesterday
toDate:midnightOfToday
withHandler:^(CMPedometerData *pedometerData, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(#"Pedometer is NOT available.");
}
else {
NSLog(#"Steps %#", pedometerData.numberOfSteps);
yesterdaysNumbersLabel.text = [pedometerData.numberOfSteps stringValue];
#synchronized (self) {
[self.pedometerDictionary setValue:[pedometerData.numberOfSteps stringValue] forKey:#"2"];
}
[self doSomethingInterestingWithDictionary:nil];
}
});
}];
}
// Will be called when queryPedometerDataFromDate returns and from a timer every 5 seconds.
- (void)doSomethingInterestingWithDictionary:(NSTimer *)aTimer {
#synchronized (self) {
NSLog(#"My days dictionary: %#", self.pedometerDictionary);
}
}