In my app I have the Restaurant class that you can see below. I'd like to attach a KVOController to it. But I'm having no luck. When I attach it with the code below, it crashes.
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;
[self.KVOController observe:self keyPath:#"[Restaurant current].name.asString" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
DDLogDebug(#"Restaurant changed");
}];
What's the best way to add KVO to a class like this?
#implementation Restaurant
static Restaurant *current = nil;
+ (Restaurant *)current {
#synchronized(self) {
if (current == nil) {
current = [[Restaurant alloc] initWithId:0];
}
}
return current;
}
- (id)initWithId:(NSInteger)number {
self = [super init];
if (self)
{
...
}
return self;
}
#end
The problem is not #synchronized. There are several issues with your code:
Do you want to observe when the current restaurant changes? Or when the current restaurant's name changes (without +[Restaurant current] pointing to a different restaurant instance). Or any kind of name change, whether triggered by a change of current or a change of name?
Depending on the answer, you'll either want to observe observe:[Restaurant class] or observe:[Restaurant instance], but definitely not observe:self (unless you're setting this up inside the Restaurant class implementation, in which case [self class] would be an alternative to [Restaurant class]).
For any change to be observable, you must ensure that the class is implemented in a KVO-compliant way. This goes both for changes to +[Restaurant current] as well as for changes to -[Restaurant name], depending on what you want to be able to observe.
[Restaurant current].name.asString is not a valid key path. Valid key paths may only contain property names (ASCII, begin with a lowercase letter, no whitespace) and dots to separate them (see Key-value coding for details). Once you're telling the KVOController to observe:[Restaurant class], all that remains for the key path is current.name.asString.
What is name if not a string? Do you really need to convert it to a string for observing it? If your intention is to watch for name changes, observing current.name is probably sufficient.
You'll likely end up with one of the following two options:
FBKVOController *kvoController = [FBKVOController controllerWithObserver:self];
[kvoController observe:[Restaurant class] keyPath:#"current.name" ...];`
// or
[kvoController observe:[Restaurant current] keyPath:#"name" ...];`
And again, for any changes to be observable, they need to be KVO-compliant.
Related
So I'm plan to create a safe init function for NSDictionary like someone else did, but as I'm a SDK developer:
I want add a switch for it, the user can decide if he want open it or not;
I don't want to use Category to implement it.
So I create a totally new class named "ALDictionarySafeFunction.h", it has two functions, the first one is the switch function, like this:
+(void)enableSafeFunction{
[ALSwizzlingHelper swizzleSelector:#selector(initWithObjects:forKeys:count:)
ofClass:NSClassFromString(#"__NSPlaceholderDictionary")
withSwizzledSelector:#selector(safeInitWithObjects:forKeys:count:)
ofClass:[ALDictionarySafeFunction class]];
}
The ALSwizzlingHelper can help me to swizzle two functions.
The second is the safe init function, like this:
-(instancetype)safeInitWithObjects:(const id _Nonnull __unsafe_unretained *)objects forKeys:(const id _Nonnull __unsafe_unretained *)keys count:(NSUInteger)cnt {
BOOL containNilObject = NO;
for (NSUInteger i = 0; i < cnt; i++) {
if (objects[i] == nil) {
containNilObject = YES;
NSLog(#"There is a nil object(key: %#)", keys[i]);
}
}
if (containNilObject) {
//Do something to make sure that it won't cause a crash even it has some nil value
}
//There is the problem, next line
[self safeInitWithObjects:objects forKeys:keys count:cnt];
}
For the normal situation(Write the swizzled method in the Category), I need to do like I wrote to invoke the original method.
But the problem is I cannot do it here, because that the "self" object is the instance of “__NSPlaceholderDictionary”, and the "__NSPlaceholderDictionary" class doesn't have the instance method "safeInitWithObjects:forKeys:count:".
So what should I do?
Is there a way to make it?
Any advice will be appreciated.
i would like to add extra data to EKEvent, i tried NSDictionary (there is a lot of data to add) but it doesn't work..
sample code:
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc]init];
[eventStore setValue:dictionary forKey:MAIN_DICTIONARY];
any ideas?
You're using setValue:forKey: in a wrong way. That a look here. There are different options to achieve what you want: category, subclassing or create a class that contains the EKEvent and the NSMutableDictionary. It depends on how you need to use the EKEvent.
You cannot do it this way, because even with key-value coding you can only set (declared or non declared) properties known by the instance. Basically the accessors (setter, getter) are executed. But there is no property MAIN_THREAD,no setter setMAIN_THREAD: in EKEvent.
If you want to extend instances of a foreign class that are created by the system (the instances, not the class), there are to ways to add data:
You create an own class, let's say MyEvent and give them a reference to the system instance (EKEvent) as a property plus the properties you need. When you get an instance of EKEvent you look-up your list of MyEventss using the identifier. With that you have the full access to your data.
You use associated objects. But you have to take care that they are not handled by the instance, i. e. while copying.
The first solution is better by far. Simple sample code:
#interface MyEvent : NSObject
#property (readonly) EKEvent* systemEvent;
#property id customProperty;
- (instancetype)eventForSystemEvent:(EKEvent*)systemEvent;
#end
#implemenation MyEvent
// Look-Up
NSMutableDictionary *eventLookUp;
+ (void)initialize
{
if( self == [MyEvent class])
{
eventLookUp = [NSMutableDictionary new];
}
}
- (instancetype)eventForSystemEvent:(EKEvent*)systemEvent
{
return eventLookUp[systemEvent.calendarItemIdentifier];
}
// Instance creation
- (instancetype)initWithSystemEvent:(EKEvent*)systemEvent
{
// Usual initializer
…
eventLookUp[systemEvent.calendarItemIdentifier] = systemEvent;
return self;
}
+ (instancetype)newEventWithSystemEvent:(EKEvent*)systemEvent
{
return [[self alloc] initWithSystemEvent:systemEvent];
}
#end
Typped in Safari
I feel like there is a more regulation way to do what I am doing in, either by some iOS specific thing, or pattern I'm aware of. I'm trying to create an NSMutableArray variable, that essentially acts as temporary storage for a logger class. Each time the array is accessed, I want to either lazily instantiate it, or set it to nil. The way I am thinking of doing it seems a little hacky and I'm looking for some input?
- (NSMutableArray)myArray {
if (!_myArray) {
_myArray = [[NSMutableArray alloc] init];
} else {
_myArray = nil;
}
return _myArray;
}
The effect I'm hoping to achieve is using a logger that is logging details about network requests - http codes, URLs, repsonse times, etc. I want the logger to amalgamate all this output in this storage array. Later on, when I'm hitting an API, I want to take the contents of this array, and send it up to the API, and I also want the array to reset (so the array is essentially a log of network request data since the last time the app hits the API, versus a log of what has happened since the app launched.
I realise that I could do this manually by niling the array when I access it, but I'm trying to do this in a more of a plug and play way, where it you don't need to worry if someone forgets to nil the array etc
The effect that you are trying to achieve is perfectly legitimate, but you shouldn't try to achieve it with a getter alone: the very fact that a simple getter could reset something back to nil would be counter-intuitive to your readers.
Instead, you should make two methods - one to prepare the array, and another one to harvest it, and replace with a fresh nil:
- (NSMutableArray*)myArray {
if (!_myArray) {
_myArray = [[NSMutableArray alloc] init];
}
return _myArray;
}
- (NSMutableArray*)resetArray{
NSMutableArray *res = _myArray;
_myArray = nil;
return res;
}
Now the sequence of operations becomes intuitively clear: you get myArray as many times as you wish, add as many items as you need, and then when you are done call resetArray. This would get you a complete array with all the data, and reset the object to be ready for the next call:
for (int col = 0 ; col != 10 ; col++) {
[log.myArray addObject:[self getDataForIndex:col]];
}
NSMutableArray* logArray = [log resetArray];
What you're doing doesn't make any sense to me.
Creating it empty if it doesn't exist makes sense.
Setting it to nil if it does exist does not make sense.
The usual pattern for lazy loading is to use a property with a getter:
#property (nonatomic, retain) NSMutableArray * myArray;
and then the implementation:
//Custom getter
-(NSMutableArray*) myArray;
{
if (!_myArray)
_myArray = [[NSMutableArray alloc] init];
return _myArray;
}
Then you ALWAYS refer to the array using the getter. If it hasn't yet been created, the getter creates and returns the empty array. If it does exist, it returns the existing one.
I know that there are tutorials everywhere, but I can't figure this out for some reason. I have a tab bar controller. Each tab links to a navigation controller, which is segued to a view controller. So, 2 main view controllers (StatusVC and TransactionsVC).
In StatusVC, I have a text field. In TransVC, I have a table view. A person adds a cell to the table. Math is done behind the scenes. The cell values are added together (numbers). This information is sent back to StatVC for calculations and displaying of the data. I've already got the math part down. My question: how do I transfer the data between view controllers, and better yet, how do I store this data so that it doesn't get deleted on quit (NSUserDefaults probably)?
This can be broken down I suppose, the transferring of data, the saving of data, and the displaying of data when the tab is pressed and view is shown.
I'm hoping this is making sense. Anyway, here's the code I've got. You're looking at TranVC. User enters data into the table with an alert view. You are looking at part of the Alert View delegate methods. This is when the user enters data into a cell (presses done). Look for key areas with the ******* comments.
StatusViewController *statVC = [[StatusViewController alloc]init]; //*******init
// Set the amount left in the budget
NSString *amountToSpend = statVC.amountLeftInBudget.text;
double budgetLabel = [amountToSpend doubleValue];
NSString *lastItem = [transactions objectAtIndex:0];
double lastLabel = [lastItem doubleValue];
double totalValue = budgetLabel - lastLabel;
NSString *amountToSpendTotal = [NSString stringWithFormat: #"%.2f", totalValue];
statVC.amountLeftInBudget.text = amountToSpendTotal; //*******set text (but not save), either way, this doesn't work
// Set the amount spent
NSString *sum = [transactions valueForKeyPath:#"#sum.self"];
double sumLabel = [sum doubleValue];
NSString *finalSum = [NSString stringWithFormat:#"%.2f", sumLabel];
//Set the amountSpent label
statVC.amountSpent.text = finalSum; //*******set text (but not save), either way, this doesn't work
// The maxed out budget section
if ([statVC.amountLeftInBudget.text isEqualToString: #"0.00"]) //*******set color (but not save), either way, this doesn't work
{
statVC.amountLeftInBudget.textColor = statVC.currencyLabel.textColor = [UIColor redColor];
} else if ([statVC.amountLeftInBudget.text compare:#"0.00"] == NSOrderedAscending)
{
statVC.amountLeftInBudget.textColor = statVC.currencyLabel.textColor = [UIColor redColor];
} else if ([statVC.amountLeftInBudget.text compare:#"0.00"] == NSOrderedDescending)
{
statVC.amountLeftInBudget.textColor = statVC.currencyLabel.textColor = [UIColor colorWithRed:23.0/255.0 green:143.0/255.0 blue:9.0/255.0 alpha:1.0];
}
if ([statVC.amountLeftInBudget.text compare:#"0.00"] == NSOrderedAscending)
{
// Create our Installation query
UIAlertView *exceed;
exceed = [[UIAlertView alloc]
initWithTitle: #"Budget Exceeded"
message: #"You have exceeded your budget amount"
delegate: self
cancelButtonTitle: #"Okay"
otherButtonTitles: nil];
[exceed show];
}
Any help with this would be amazing.
This is indeed a common question.
There are various solutions. The one I recommend is to use a data container singleton. Do a google search on the singleton design pattern in Objective C. You'll even find examples of it here on SO.
Create a singleton with properties for the values that you want to share. Then teach your singleton to save it's data. You can use user defaults, you can use NSCoding, you can extract the data to a dictionary and save it to a plist file in your documents directory, or various other schemes as well.
Like Duncan suggested, a Singleton pattern might be the best route to go. If you place the shared data into a model class, you can create a class method that can be used to acquire a singleton object.
MyModel.m
#implementation MyObject
- (id) init
{
return nil; // We force the use of a singleton. Probably bad practice?
}
// Private initializer used by the singleton; not included in the header file.
- (id)initAsSingleton {
self = [super init];
if (self) {
// Initialize your singleton instance here.
}
return self;
}
+ (MyModel *)sharedMyModel {
static MyModel *myModel = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
myModel = [[MyModel alloc] initAsSingleton];
});
return myModel;
}
MyModel.h
#interface MyModel : NSObject
+ (MyModel *)sharedMyModel; // Singleton instance.
#end
This does not protect against you using [[MyModel alloc] init];. It returns a nil object which is probably poor programming on my end, but it does force you to use the singleton object instead. To use in each one of your view controllers, you just use the following line to grab the singleton instance.
MyModel *model = [MyModel sharedMyModel];
Store the data into it, and return to your other view controller and grab the singleton again. You'll have all of your data.
After thinking about it, you could also force the default initializer to just return your singleton instance like:
- (id)init {
return [MyModel sharedMyModel];
}
I am accessing a dispatched notification like so:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleUnpresent:) name:UNPRESENT_VIEW object:nil];
...
-(void)handleUnpresent:(NSNotification *)note;
{
NSLog(#"%#", note.object.footer);
//property 'footer' not found on object of type 'id'
}
Some of the incoming note.object objects have a "footer" and some don't. However, I don't want to go through to trouble of making a class that only has a property called footer just to make this work. I even tried ((NSObject *)note.object).footer) which works in some languages, but apparently not obj-c. What can I do?
Checking the isKindOfClass is certainly the more robust option. However, if you have multiple unrelated classes that return the property you need, there is another way: respondsToSelector. Just ask if the object has a footer method, and you can safely call it.
-(void)handleUnpresent:(NSNotification *)note;
{
id noteObject = [note object];
if ([note respondsToSelector:#selector(footer)])
{
NSLog(#"Footer = %#", [noteObject footer]);
}
}
That respondsToSelector method is powerful and handy in the right places, but don't go wild with it. Also, it can't tell you anything about the return type, so the footer you get may not be of the class you were expecting.
The syntax for noteObject.footer and [noteObject footer] are easy to treat as equivalent. However, when the class of noteObject is unknown, the compiler will accept the latter but not the former. If noteObject has a defined class that doesn't usually respond to footer, it will give a warning, but still compile and run. In these cases, it is your responsibility to guarantee that the method will indeed exist when needed, and therefore that the method call won't crash at run time.
If the object passed in the notification may be one of a number of classes and you don't want to cast the object to a specific class you can use performSelector: to call the footer method on the object. If you wrap this call with a respondsToSelector: you'll avoid an exception if the object turns out not to have a footer method.
-(void)handleUnpresent:(NSNotification *)note;
{
if ([[note object] respondsToSelector:#selector(footer)]) {
NSString *footer = [[note object] performSelector:#selector(footer)];
NSLog(#"%#", footer);
}
}
Using performSelector will stop the compiler complaining that the method "'footer' not found on object of type 'id'."
NSObject doesn't have any property named footer, which is why the compiler is complaining. Casting an id back to an NSObject doesn't help. If you know the object is always going to be some custom object you've created, you can cast back to that and then call footer and the compiler won't complain. It's best to actually check tho. See the example below (for the example, I named the class that has the footer property ViewWithFooter, so rename appropriately):
- (void)handleUnpresent:(NSNotification*)note
{
ViewWithFooter view = (ViewWithFooter*)[note object];
NSParameterAssert([view isKindOfClass:[ViewWithFooter class]]);
UIView* footer = [view footer];
// Do something with the footer...
NSLog(#"Footer: %#", footer);
}
If you have a bunch of unrelated classes (i.e., not in the same class hierarchy) that all present a footer property, you'd be best served creating a protocol with the required footer property and casting the object to the protocol in the code example above and asserting it responds to the -footer selector.
Here's an example using the protocol:
#protocol ViewWithFooter <NSObject>
- (UIView*)footer; // this could also be a readonly property, or whatever
#end
- (void)handleUnpresent:(NSNotification*)note
{
id<ViewWithFooter> view = (id<ViewWithFooter>)[note object];
NSParameterAssert([view respondsToSelector:#selector(footer)]);
UIView* footer = [view footer];
// Do something with the footer...
NSLog(#"Footer: %#", footer);
}