Sunday, May 22, 2011

UITabBarController with custom UITabBarItem colors


In the last few days my team and I were tasked with creating an app for a brand which has orange colors. We wanted a tab bar. We wanted the selected tab bar item to highlight in orange rather than the default aqua color. If you're reading this than you probably already know that a straightforward way to achieving this using UITabBarController doesn't exist. In this post I'd like to go through the code I've written for a custom tab bar controller that allows you to set custom image masks for your highlighted items. The project hasn't been implemented to fully replicate the functionality of UITabBarController, however the basics are in and you can add additional features if you like.


What does the project consist of?


The project consists of 3 classes as shown below.


How do you use it?


Very simple. You use it just like you would use UITabBarController with two exceptions:

  1. If you're loading your current tab bar controller from a NIB file you will need to set it up programatically.
  2. You need to set up the image masks used for selected and unselected items.

So where do you start? Download the project code here. Copy the files from the TATabBarController folder into your project and change all references of UITabBarController to TATabBarController. Once you've done that you're left with going through 4 easy steps to get things up and running.



Don't forget to set the tabBarItem property for the view controllers you're adding to the tab bar controller - otherwise nothing will appear.


How does it work?


Once you set the view controllers the magic begins to happen. Below I'll describe how I went about drawing the tab bar images and text. The rest of the details regarding the functioning of the tab bar will be left out of the scope of this post. The code that shows how all this is done is located in TATabBars - (UIImage*)_tabBarImage:(UIImage*)tabBarImage withGradient:(UIImage*)gradientImage method.



Additionally, if we are drawing the selected tab bar item we apply a shadow to the image and text.


You can download the project code here. Have fun!

Saturday, April 30, 2011

Game Center invitation errors. Two things often overlooked when dealing with GKInvite.

The past few days I've been working on integrating Snooker Club with most of Game Kit's functionality (leaderboards, achievements, multiplayer, voice chat). Having done this before it went like a breeze. The multiplayer capabilities add a lot to the fun and playability of the game... if they work. In certain situations the game wouldn't connect with other players, it wouldn't even acknowledge that an invite had been received. It didn't take long to sort it out, but perhaps I can save you a little bit of time by outlining the 2 most common scenarios that are overlooked in Apple's Game Kit guide.


Scenario 1


Inviting a friend works fine when you're testing? The app is off, you get an invite, the app launches and manages to connect. Funnily enough, when testing on a larger user base it would seem that when friends in close proximity want to challenge each other they tend to wait for their invites whilst GKMatchmakerViewController is already being displayed. In the example Apple provides your code for handling an invite should look like this:


[GKMatchmaker sharedMatchmaker].inviteHandler = ^(GKInvite *acceptedInvite, NSArray *playersToInvite) {
   // Insert application-specific code here to clean up any games in progress.
   if (acceptedInvite)
    {
        GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithInvite:acceptedInvite] autorelease];
        mmvc.matchmakerDelegate = self;
        [self presentModalViewController:mmvc animated:YES];
    }
    else if (playersToInvite)
    {
        GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease];
        request.minPlayers = 2;
        request.maxPlayers = 4;
        request.playersToInvite = playersToInvite;
 
        GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];
        mmvc.matchmakerDelegate = self;
        [self presentModalViewController:mmvc animated:YES];
    }
};

The code above doesn't take into account that you might already be displaying an instance of GKMatchmakerViewController as a modal view. Trying to display a new instance of GKMatchmakerViewController initialized with the new invite and displaying it as a modal view controller leads to nothing. In order to handle this particular scenario you need to dismiss the visible modal view controller before presenting the new one, like so:


[GKMatchmaker sharedMatchmaker].inviteHandler = ^(GKInvite *acceptedInvite, NSArray *playersToInvite) {
    UIViewController *topLevelViewController = (UIViewController*)[[(GameHostAppDelegate*)[UIApplication sharedApplication] delegate] gameHostViewController];
    // Insert application-specific code here to clean up any games in progress.

    if (acceptedInvite)
    {
     isInviter = NO;
     GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithInvite:acceptedInvite] autorelease];
     mmvc.matchmakerDelegate = self;
     
     if([topLevelViewController modalViewController] != nil)
      [topLevelViewController dismissModalViewControllerAnimated:NO];

     [topLevelViewController presentModalViewController:mmvc animated:YES];
    }
    else if (playersToInvite)
    {
     isInviter = NO;
     GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease];
     request.minPlayers = 2;
     request.maxPlayers = 2;
     request.playersToInvite = playersToInvite;
     
     GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];
     mmvc.matchmakerDelegate = self;
     
     if([topLevelViewController modalViewController] != nil)
      [topLevelViewController dismissModalViewControllerAnimated:NO];

     [topLevelViewController presentModalViewController:mmvc animated:YES];
    }



Make sure you don't animate the view controller being dismissed, otherwise the new instance of GKMatchmakerViewController will not show up.


Scenario 2


You've applied the fix for the issue mentioned above. Things work much better, but still feel a little unreliable? Try this.

  • Launch your app.
  • Put your app into the background.
  • Send an invite to yourself from another device.
  • Accept the invite.

If the invite is handled correctly go through the steps again, this time on a slower connection. Did you notice how unreliable your invitation handler has become? Apple's documentation states the following: Important:Your application should provide an invitation handler as early as possible after your application authenticates the local player; an appropriate place to set the handler is in the completion handler that executes after the local player is authenticated. It is critical that your application authenticate the local player and set the invitation handler as soon as possible after launching, specifically so that you can handle the invitation that caused your application to be launched.

There is a very important thing to extract from Apples info. Your application should provide an invitation handler as early as possible after your application authenticates the local player. You don't want to have an invitation handler in place if your user is not authenticated. This becomes a bit tricky with multitasking.

If you're on a device that supports multitasking you need to authenticate every time your app enters the foreground (as of iOS 4.2 this is handled automatically). You're probably already doing something like:

- (void)applicationWillEnterForeground:(UIApplication *)application
{
 [gameCenter validateAuthentication];
}

where validateAuthentication checks to see if you're still logged in as the same player, that you're authenticated and if not tries to authenticate you.

This leads to an interesting situation, which results in unreliable handling of invites. Look at the 4 steps above. When you accept an invitation, your app goes into the foreground. The [GKMatchmaker sharedMatchmaker].inviteHandler is called but the player has most probably not been authenticated yet. Nothing happens. Whats the solution, you ask? Simple.

In your application's delegate method - (void)applicationDidEnterBackground:(UIApplication *)application simply add this line:

[GKMatchmaker sharedMatchmaker].inviteHandler = nil;

This way the invite handler won't be called until the player is authenticated. Once the player is authenticated the inviteHandler will be created once again, assuming you've followed Apple's guides and added the code to create the handler after the user authenticates ( in [localPlayer authenticateWithCompletionHandler:^(NSError *error) ).

I hope that the solutions to these 2 common scenarios bring you closer to solving reliability issues with Game Center.

Saturday, February 19, 2011

NSBundle vs. UINib performance

Amongst many wonders in iOS 4.0 you will find a class named UINib. It's purpose - to optimise the loading time of nibs. It has been rumored to be much faster than NSBundle. In this post I set out to compare the loading times for nib files using good ol' NSBundle (or to be more precise the methods provided by the "UINibLoadingAdditions" NSBundle category) and the younger UINib.


I've created a simple test case in which a table view displays cells that consist of 2 labels. The cell has been designed in Interface Builder and saved in it's very own nib file. Once the table view asks for a cell we load the nib file, instantiate the object graph and voilĂ ! We end up with a beautiful table view displaying 11 cells. This also means that my table view will need to create 11 instances of my cell. I better find the most optimal way to load that nib!


I start out by using the NSBundle loading mechanism. In my - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath method I load the nib, measure the time it took to load, find the table view cell and set some values for its 2 labels.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ExampleCell"];

if(cell == nil)
{
NSDate *startDate = [NSDate date];
NSArray *bundleObjects = [[NSBundle mainBundle] loadNibNamed:@"ExampleCell" owner:nil options:nil];
printf("%f\n", [[NSDate date] timeIntervalSinceDate:startDate]);

for(id obj in bundleObjects)
{
if([obj isKindOfClass:[UITableViewCell class]])
cell = (UITableViewCell*)obj;
}
}

[(UILabel*)[cell viewWithTag:1] setText:[NSString stringWithFormat:@"%d", [indexPath section]]];
[(UILabel*)[cell viewWithTag:2] setText:[NSString stringWithFormat:@"%d", [indexPath row]]];

return cell;
}


I ran the code 3 times on an iPhone 4. On average the time it took to load each of the 11 cells in milliseconds was:

0.016110333
0.006223667
0.004825
0.004784333
0.005055667
0.004709333
0.004510333
0.004458667
0.004389
0.004378333
0.004712667

Notice how the first loading takes a considerably larger amount of time than all the next ones. I haven't found any information mentioning NSBundle doing any sort of caching so I'm presuming the reason why every subsequent call takes less time is due to the underlying file system methods it uses.



Next, I replaced NSBundle with UINib like so:


if(cell == nil)
{
NSDate *startDate = [NSDate date];
if(!nib_)
nib_ = [UINib nibWithNibName:@"ExampleCell" bundle:nil];

NSArray *bundleObjects = [nib_ instantiateWithOwner:nil options:nil];

printf("%f\n", [[NSDate date] timeIntervalSinceDate:startDate]);

for(id obj in bundleObjects)
{
if([obj isKindOfClass:[UITableViewCell class]])
cell = (UITableViewCell*)obj;
}
}


Notice how the UINib has the loading and instantiation methods seperated. This provides an immense performance boost, because we only have to load the nib file from disk once and with each subsequent call to the table views delegate method only instantiate a new object graph using the cached nib data.

Same as before, I ran this 3 times and here is what I got:

0.015249333
0.002955
0.002447
0.002064667
0.002092667
0.001903
0.001851667
0.001981
0.001852667
0.001873333
0.001941667

Again, the first loading time is much higher than the subsequent ones.



So how do the 2 compare?



We can see that both start out at a similar point. With each subsequent call both UINib and NSBundle loading times remain at their respective levels. It's at this point where it becomes obvious that UINib beats NSBundle hands down. The fact that UINib keeps nib data in memory proves to be it's winning point if you're instantiating the same nib more than once.

Friday, May 15, 2009

F-Script - an essential tool for cocoa developers

I've read about F-Script at least 5 times before I actually decided to give it a try. What convinced me is that every one who had the opportunity to see it in action seemed to be under a big impression. Soon enough the time came for me to be under impression too.

It was so easy to set up I started playing around with it in a matter of minutes. The possibilities are pretty much endless. Think about it as programming (and debugging) objective-c applications dynamically during runtime. You can tinker around with your controls, see what data you're storing without having to use a debugger, check your bindings and core data objects.



I'd like this post to encourage you to give F-Script a try. In order to achieve this I'll tell you how to set everything up and how to start using F-Script from within the application you are developing. This shouldn't take more than 5 minutes.

1) Step 1 - Download F-Script from www.fscript.org
2) Install the F-Script interface builder plugin

Go into the F-Script folder you downloaded and copy FScriptIBPlugin.ibplugin to /Developer/Platforms/MacOSX.platform/Developer/Library/Interface Builder/Plug-ins/

3) Open your XCode project and add FScript.framework to the project frameworks.




Locate the framework and click "Add".

4) Set up your project to copy the FScript.framework to the target when compiling.


Choose the target and from the context menu choose "New Copy Files Build Phase". This will create a new build phase which will always copy the files you specify when building the project.


From the "Destination" popup menu choose "Frameworks" and close the window.


Position the new build phase just below the "Copy Bundle Resources" phase (although to tell you the truth I haven't ever really checked if order is important, I just assume it is as it doesn't hurt), rename it to "Copy Frameworks" and drag the FScript.framework file you previously added to your projects frameworks to the new build phase.

5) Open MainMenu.nib (or .xib) and drag the FScript menu control from the control library to your menu.


Locate the FScript menu item in the Interface Builder library palette.


Drag and drop it to the main menu of your application.

6) Build and Run your application. From the main menu choose F-Script -> Object browser. A new window will open. Click "Select view" and click on a view which properties you wish to observe. Enjoy!

Monday, May 11, 2009

Binding NSArrayControllers arrangedObjects to custom NSView

Some functionality in cocoa is taken for granted. It's sooo easy to create something that works - drag here, drop there, bind, bind, presto! Some time during your adventure with cocoa you might start wondering how some of these mechanisms work. I did :) When that moment comes, don't back down. Instead, try pursuing the idea. It's a great time to delve deeper into cocoa and objective-c and see how apple engineers solved issues you might have been implementing wierd workarounds for.


I like the way some people write about cocoa's bindings - "It's magic". It sure does seem so for some one who knows nothing about them (or knows a whole bunch and doesn't feel like explaining). The truth is bindings are nothing more than an automated form of KVO (Key-Value Observing). Binding an object to another object results in those objects being in sync. We can do the exact same thing by observing those objects and manually updating their values, but in such a case we would end up writing a lot of our own glue code.


Recently I've created a custom NSView that essentially mimicked the way NSTableView worked. I didn't want to subclass NSTableView mainly because it seems to have been designed to work well when inside an NSScrollView, not as an independent view. Seeing as how NSTableView had a content property you could bind to I created an NSMutableArray object called content in my class and made it KVO compliant. This is where the problems started. Simply binding content to my NSArrayControllers arrangedObjects array wasn't going to inform my custom view about the changes that were being made to arrangedObjects. I figured I could overcome this by observing arrangedObjects. Unfortunately, it turns out that key-value observing for collections isn't fully functional. Apples documentation for:



- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;


states that:


NSKeyValueChangeKindKey

An NSNumber object that contains a value corresponding to one of the NSKeyValueChangeKindKey enumerations, indicating what sort of change has occurred.

A value of NSKeyValueChangeSetting indicates that the observed object has received a setValue:forKey: message, or that the key-value-coding-compliant set method for the key has been invoked, or that willChangeValueForKey:/didChangeValueForKey: has otherwise been invoked.

A value of NSKeyValueChangeInsertionNSKeyValueChangeRemoval, or NSKeyValueChangeReplacement indicates that mutating messages have been sent to the array returned by amutableArrayValueForKey: message sent to the object, or that one of the key-value-coding-compliant array mutation methods for the key has been invoked, or thatwillChange:valuesAtIndexes:forKey:/didChange:valuesAtIndexes:forKey: has otherwise been invoked.

You can use NSNumber's intValue method to retrieve the integer value of the change kind.

Available in Mac OS X v10.3 and later.

Declared in NSKeyValueObserving.h.

NSKeyValueChangeNewKey

If the value of the NSKeyValueChangeKindKey entry is NSKeyValueChangeSetting, and NSKeyValueObservingOptionNew was specified when the observer was registered, the value of this key is the new value for the attribute.

For NSKeyValueChangeInsertion or NSKeyValueChangeReplacement, if NSKeyValueObservingOptionNew was specified when the observer was registered, the value for this key is an NSArray instance that contains the objects that have been inserted or replaced other objects, respectively.

Available in Mac OS X v10.3 and later.

Declared in NSKeyValueObserving.h.


If this were true, I wouldn't be writing this post :) If the value of NSKeyValueChangeKindKey for the change dictionary contains NSKeyValueChangeInsertion, NSKeyValueChangeRemoval or NSKeyValueChangeReplacement the value for NSKeyValueChangeNewKey will always be [NSNull null]. From what I've read, this is something Apple is aware of, but hasn't established a date for when it will be fixed (this could mean that it will never get fixed).


What this implied for my custom view is the fact that I needed to establish what the changes were myself. However, if I bind content to arrangedObjects and observe arrangedObjects the value of both will always be the same once my code in the observing method is reached. This in turn implies that I will not be able to get the changes and that bindings will not be a feasible solution for this problem. I started wondering how Apple solved this issue. What I came up with is a solution that perfectly mimics NSTableViews behaviour.


I came to terms with the fact that I won't be able to use bindings, but I really wanted the illusion of bindings to stay. I decided to overwrite the method used for binding like so:


- (void)bind:(NSString *)binding toObject:(id)observableController withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
{
if([binding isEqualToString:@"content"] && [keyPath isEqualToString:@"arrangedObjects"] && [observableController isKindOfClass:[NSArrayController class]])
{
[observableController addObserver:self forKeyPath:keyPath options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
_contentArrayController = observableController;
[self setContent: [_contentArrayController arrangedObjects]];
}
else
[super bind:binding toObject:observableController withKeyPath:keyPath options:options];
}


From the outside this still looks like the normal binding procedure is taking place, when in reality it's only observing. I also assigned my NSArrayController to a class variable in my custom view called _contentArrayController. This becomes useful in the unbind method:


- (void)unbind:(NSString*)keyPath
{
if([keyPath isEqualToString:@"content"])
{
if(_contentArrayController != nil)
{
[_contentArrayController removeObserver:self forKeyPath:@"arrangedObjects"];
_contentArrayController = nil;
}
}

[super unbind:keyPath];
}


Once all that is done you can try to observe changes in the observeValueForKeyPath method. What you will notice is that there is another issue - the only value the change dictionary will return for the NSKeyValueChangeKindKey is NSKeyValueChangeSetting. This is due to the way the arrangedObjects array is set in NSArrayController. So what we are left with is determining changes ourselves. If you need a custom view that displays a small amount of data then you might as well just set the new value for your views content based on what arrangedObjects contains. If you NEED to know what changed then I guess the reasonable way to go about it would be to compare your views current content array with the new arrangedObjects array like so:


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualToString:@"arrangedObjects"] && _contentArrayController != nil)
{
NSMutableArray *array = [_contentArrayController arrangedObjects];

if(array != nil)
{
if([array count] > [content count])
{
NSLog(@"items added");
}
else if([array count] < [content count])    {     NSLog(@"items removed");    }    else if([array count] == [content count])    {     NSLog(@"items replaces");    }   }                 //don't forget to set the content array!!!   [self setContent:array];   [self setNeedsDisplay:YES];  } } 

The problem with this is that if you assign a completely new array to NSArrayControllers content the above code won't be worth a damn. You could ofcourse go through every object in the array every time arrangedObject changes, but for large amounts of data that seems like a bad use of resources.

Anyhow, I encourage readers to look for interesting solutions to fun problems.

Wednesday, April 22, 2009

PDF with paging

In my previous post I wrote about how to render the contents of an NSView to a pdf. That method was simple and intuitive. However, it had 1 drawback. It didn't allow me to create paginated PDF's. The way you go about this is much less intuitive and requires you use the NSPrintOperation class. Basically, what you want to do is print to a PDF. You start by creating an NSPrintInfo object using the default printing preferences:


//this will point to our NSPrintInfo object
NSPrintInfo *printInfo;
//this will point to the default printer info object
NSPrintInfo *sharedInfo;
//thi will point to our settings for the NSPrintInfo object
NSMutableDictionary *printInfoDict;
//this will point to the settings for the default NSPrintInfo object
NSMutableDictionary *sharedDict;

sharedInfo = [NSPrintInfo sharedPrintInfo];
sharedDict = [sharedInfo dictionary];
printInfoDict = [NSMutableDictionary dictionaryWithDictionary:
sharedDict];

//below we set the type of printing job to a save job.
[printInfoDict setObject:NSPrintSaveJob 
forKey:NSPrintJobDisposition];

//set the path to the file you want to print to
[printInfoDict setObject:@"/Users/OnCocoa/Desktop/test.pdf" forKey:NSPrintSavePath];

//create our very own NSPrintInfo object with the settings we specified in printInfoDict
printInfo = [[[NSPrintInfo alloc] initWithDictionary: printInfoDict] autorelease];


Once you set up your NSPrintInfo object you have to create an NSPrintOperation object, specifying which view you want to print from and which NSPrintInfo object you want to use.


//create the NSPrintOperation object, specifying docView from the previous post as the NSView to print from.
NSPrintOperation *printOp = [NSPrintOperation printOperationWithView:docView printInfo:printInfo];

//we don't want to show the printing panel
[printOp setShowPanels:NO];

//run the print operation
[printOp runOperation];


Thats all you need to do to print a view to a PDF with paging the vanilla way. If you want to print like Safari does, you have to set up the margins appropriately using the code below:


[printInfo setHorizontalPagination: NSFitPagination];
[printInfo setVerticallyCentered:NO];
[printInfo setHorizontallyCentered:NO];

NSRect imageableBounds = [printInfo imageablePageBounds];
NSSize paperSize = [printInfo paperSize];
if (NSWidth(imageableBounds) > paperSize.width) {
imageableBounds.origin.x = 0;
imageableBounds.size.width = paperSize.width;
}
if (NSHeight(imageableBounds) > paperSize.height) {
imageableBounds.origin.y = 0;
imageableBounds.size.height = paperSize.height;
}

[printInfo setBottomMargin:NSMinY(imageableBounds)];
[printInfo setTopMargin:paperSize.height - NSMinY(imageableBounds) - NSHeight(imageableBounds)];
[printInfo setLeftMargin:NSMinX(imageableBounds)];
[printInfo setRightMargin:paperSize.width - NSMinX(imageableBounds) - NSWidth(imageableBounds)];


One more thing to keep in mind is that if you are generating a PDF from HTML using a WebView (like in my previous post) you have to wait for the contents of the WebView to render. It doesn't matter if you are loading from disk or from the net, you should always set a frame delegate using WebViews -(void)setFrameLoadDelegate:(id)delegate and respond to:


- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame;


This method will get called once the WebView renders the contents of the page you requested. Check out the screenshot below to see what you can accomplish with all this:




Good luck!

Sunday, April 19, 2009

HTML to PDF using WebKit

Creating a PDF with the contents of any NSView is extremely easy. All you have to do is use NSViews:


- (NSData *)dataWithPDFInsideRect:(NSRect)aRect


When trying to create a PDF from HTML the natural way to go is using the method mentioned above from an instance of WebView. Unfortunately, it turns out there is a small inconvenience. When called from an instance of WebView this method WILL NOT draw everything to PDF. It's very easy to test this without writing any code. Try printing a few pages to PDF in Safari. This is how my blog gets rendered:




It's also interesting that google.pl renders without the google logo, while google.us renders properly:




Anyhow, this issue is very easy to solve in code. Let's say you load your PDF as follows:


//get a pointer to the document view so that we render the entire web page, not just the visible portion.
NSView *docView = [[[webview mainFrame] frameView] documentView];

[docView lockFocus];

//create the PDF
NSData *data = [docView dataWithPDFInsideRect:[docView bounds]];

[docView unlockFocus];

//create an instance of a PDFDocument to display in a PDFView.
PDFDocument *doc = [[PDFDocument alloc] initWithData:data];

//display the PDF document in a PDFView
[pdfview setDocument:doc];

[pdfWindow orderFront:nil];


This will produce the standard results. In order to render everything to PDF you have to set WebViews preferences appropriately. WebView has a method - (void)setPreferences:(WebPreferences*)preferences. If you look at the documentation for WebPreferences you will notice it has a method called - (void)setShouldPrintBackgrounds:(BOOL)pb. Setting this to YES solves the minor inconvenience mentioned in this post.


WebPreferences *preferences = [[[WebPreferences alloc] initWithIdentifier:@"testing"] autorelease];
[preferences setShouldPrintBackgrounds:YES];
[webview setPreferences:preferences];


After setting WebViews preferences to print the backgrounds, my blog and all other pages print just fine to PDF.



UPDATE:

Check out my next post where I mention two important things:
1) How to print to a PDF with paging.
2) How to make sure the WebView loaded all it's content before printing (if you're getting a blank page when printing this is what you need!).