Faster way to load Nib file from a bundle in iOS? - objective-c

I have an app which utilises a set of custom "controls" which are loaded on demand from Xib files using methods similar to the below:
NSArray * topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"AudioPlayer" owner:self options:nil];
InteractiveMovieView *newAudio = [topLevelObjects objectAtIndex:0];
This approach works great except where there are multiple controls loaded at once (in effect on one "page" of the app).
Loading from the bundle each time is clearly inefficient but I can't find another way of approaching this. I've tried loading the nib into a copy property once and returning it on demand for re-use, but that doesn't work as the copy returned is never a "clean" copy of the blank nib.
I hope that makes sense, and all help is appreciated.

It sounds like you're looking for the UINib class. From the documentation:
Your application should use UINib objects whenever it needs to repeatedly instantiate the same nib data. For example, if your table view uses a nib file to instantiate table view cells, caching the nib in a UINib object can provide a significant performance improvement.

Following Rob's suggestion, you could do the following:
#implmentation InteractiveMovieView (NibFactory)
+(id)movieView
{
static UINib * __nib ;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
__nib = [ UINib nibWithNibName:#"AudioPlayer" bundle:nil ] ;
});
InteractiveMovieView * view = [ __nib instantiateWithOwner:nil options:nil ][0] ;
return view ;
}
#end

Related

How do I load a nib from a secondary process?

I have a process spawned with NSTask that needs to display a window. I started out just writing all the UI code by hand, but this has become a pain.
So, I created a new class with a xib, MyWindowController. I want to load up an instance of this controller in secondary process and have all the IBOutlets and whatnot work properly.
Here's what I've got so far:
// Get the bundle for the main application (not the subprocess).The executable lives in Contents/Helpers, so look two dirs up from its path for the main app bundle root.
NSArray *executablePathComponents = [[[NSBundle mainBundle] executableURL] pathComponents];
NSIndexSet *indexOfEveryComponentExceptLastTwo = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [executablePathComponents count] - 2)];
NSBundle *myBundle = [NSBundle bundleWithURL:[NSURL fileURLWithPathComponents:[executablePathComponents objectsAtIndexes:indexOfEveryComponentExceptLastTwo]]];
// Load the controller nib.
NSNib *windowControllerNib = [[NSNib alloc] initWithNibNamed:#"MyWindowController" bundle:myBundle];
MyWindowController *windowController = [[MyWindowController alloc] init];
NSArray *topLevelObjects = nil;
[windowControllerNib instantiateNibWithOwner:windowController topLevelObjects:topLevelObjects];
This gives me an instance of the window controller and it displays the window from the nib on screen, so this appears to work. BUT, instantiateNibWithOwner:topLevelObjects is deprecated in favor of instantiateNibWithOwner:topLevelObjects.
Using the non-deprecated method results in an exception: "-[NSNib instantiateWithOwner:topLevelObjects:]: unrecognized selector sent to instance 0x10291ab1"
At the very least I'd like to not use the deprecated method. But maybe there is a better way to approach this whole thing?

Why isn't [NSBundle mainBundle] working here?

I've never loaded a bundle, so I'm not sure why this is not working. I don't think it matters, but the .xib in question here is in the same Resources folder as all my other .xibs.
NSArray *array = [[NSBundle mainBundle] loadNibNamed:#"S3AsyncView" owner:self];
Returns this error:
Instance method -loadNibNamed:owner not found. Return type defaults to id
I find this error strange, because the return type of [NSBundle mainBundle] is of course NSBundle.
There is no such method in NSBundle, hence the error.
I guess you are looking for:
loadNibNamed:owner:options:
Documentation link
You can pass nil to the options, as it expect a NSDictionary
So in your case:
NSArray *array = [[NSBundle mainBundle] loadNibNamed:#"S3AsyncView" owner:self options:nil];
EDIT
If it still doesn't work, verify you have included <UIKit/UIKit.h>.
EDIT 2
Ok, now I see. You tagged your question with iOS, but now you say it's a Cocoa app.
The loadNibNamed:owner:options: is a UIKit addition, so available only on iPhone.
On Mac OS X, you'll use the + (BOOL)loadNibNamed:(NSString *)aNibName owner:(id)owner class method.
So:
NSArray *array = [ NSBundle loadNibNamed: #"whatever" owner: self ];
Three things:
Make sure that you're spelling the method name right. The error message you give shows the method name as: -loadNibNamed:owner:options, which isn't right. There should be a colon after the "options". Perhaps you missed that in pasting the name into your message, but the lesson here is to check carefully that you're using exactly the right method name, with no spelling errors, omitted parts, missing colons, etc.
Make sure that you're linking against UIKit. NSBundle is part of the Foundation framework, but the -loadNibNamed:owner:options: method comes from a UIKit Additions category on NSBundle that's part of UIKit. If you don't link against UIKit, then, NSBundle won't have that method.
I see that you've removed ios from your list of tags. If you're writing for Cocoa and trying to load a nib, see the NSNib class for some convenient methods for loading nibs.
I have come across the very same problem while fixing an issue in a low-level Cocoa/Objective-C++ framework. Strictly speaking, build issue came from this function:
bool osxNibLoadMenuNibFile()
{
const auto cvAppKitVersion = floor( NSAppKitVersionNumber );
if( cvAppKitVersion >= NSAppKitVersionNumber10_8 )
{
NSBundle * mainBundle = [NSBundle mainBundle];
NSDictionary * bundleInfoDict = [mainBundle infoDictionary];
if( bundleInfoDict != nil )
{
NSString * mainNibFleNameStr = [bundleInfoDict valueForKey:#"NSMainNibFile"];
if( mainNibFleNameStr != nil )
{
if( [mainBundle loadNibNamed:mainNibFleNameStr owner:[NSApplication sharedApplication] topLevelObjects:nil] )
{
return true;
}
}
}
}
return false;
}
Clang gave me:
warning: instance method '-loadNibNamed:owner:topLevelObjects:' not found (return type defaults to 'id') [-Wobjc-method-access]
The issue was not a build configuration, as all standard frameworks were there already. The issue was more trivial: the definition of that single method is present in a separate header. So please be sure to add:
#import <AppKit/NSNibLoading.h>
which contains:
#interface NSBundle(NSNibLoading)
- (BOOL)loadNibNamed:(NSNibName)nibName owner:(nullable id)owner topLevelObjects:(NSArray * _Nullable * _Nullable)topLevelObjects API_AVAILABLE(macos(10.8));
#end
Interestingly enough, CLion gaves me "unused import directive" even though I definitely use it. Hope this helps someone!

reloadRowsAtIndexPaths stops loading UITableViewCells?

I'm trying to incorporate favicons into a UITableView. The table basically fetches websites, and I want to display the favicon onto the right. I put a placeholder icon at the right initially and let a function in the background run. This function takes the URL of the website, parses it and attempts to find the favicon. If it can't find it, it keeps the same placeholder image; otherwise, it replaces it with the site's favicon. I initially tried using [tableView reloadData] which worked well in the simulator, but it did really odd and unreliable things (like for instance, it would create some cells, but then leave a giant, blank cell). Anyway, I stumbled upon reloadRowsAtIndexPaths, and it seems like the function I need to use. However, the results are still pretty unreliable. I have my fetching function running in the background as such:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...//other code here for showing website labels
//obtain the favicon.ico
if(!this_article.iconLoaded){
this_article.iconLoaded = TRUE;
NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:this_article, #"article", indexPath, #"indexPath", nil];
[self performSelectorInBackground:#selector(fetchFaviconWrapper:) withObject:args];
}
cell.favicon.image = this_article.icon;
return cell;
}
in FetchFaviconWrapper:
- (void)fetchFaviconWrapper:(NSDictionary *)args {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self fetchFavicon:[args objectForKey:#"article"]];
NSArray *paths = [NSArray arrayWithObject:[args objectForKey:#"indexPath"]];
[articleTable beginUpdates];
[articleTable reloadRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationFade];
[articleTable endUpdates];
[pool release];
}
Basically, fetchFavicon takes a website, takes the host URL, appends "/favicon.ico", constructs it into an NSData object, and finds the image (if it exists). However, this has also been pretty unreliable. What would be the best way to replace the placeholder image while running a thread in the background? I could do everything on the main thread, but that just makes the table load slowly. There seems to be something that I'm overlooking, or something that I just forgot to add...just can't figure it out.
It is not entirely clear if you are accessing and modifying your UI from the separate thread.
If so, this is the cause of your unreliability. UIKit can be accessed only from the main thread. If you are interested you will find many questions on S.O. and many discussions on the web.
There is a workaround, if you want to keep your second thread. Indeed, you can send messages to your UI object using:
-performSelectorOnMainThread:withObject:waitUntilDone:
instead of sending them directly from the secondary thread.
If this workaround does not solve the issue for you, then I would suggest redesigning your app so that the secondary thread only access your model, without accessing the UI. All the operations that modify the UI should be executed on the main thread. If you need to call reloadData on your table when the model is ready, you can do it using performSelectorOnMainThread:withObject:waitUntilDone.
I'd suggest a few things.
Appending ./favicon.ico isn't always accurate. Look into various methods of adding favicons to sites to support them.
As far as replacing the default placeholder, I suggest using the NSNotificationCenter to inform the main thread when to make the changes.
I definitely reiterate the no UI from background thread.
change your initial code:
- (void)fetchFaviconWrapper:(NSDictionary *)args {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self fetchFavicon:[args objectForKey:#"article"]];
NSArray *paths = [NSArray arrayWithObject:[args objectForKey:#"indexPath"]];
[articleTable beginUpdates];
[articleTable reloadRowsAtIndexPaths:paths
withRowAnimation:UITableViewRowAnimationFade];
[articleTable endUpdates];
[pool release];
}
to this, and see if you're still having the problem
- (void)fetchFaviconWrapper:(NSDictionary *)args {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self fetchFavicon:[args objectForKey:#"article"]];
NSArray *paths = [NSArray arrayWithObject:[args objectForKey:#"indexPath"]];
[self performSelectorOnMainThread:#selector(reloadData)
withObject:nil
waitUntilDone:YES];
[pool release];
}
The waitUntilDone is irrelevant in this case, and it's preferred form to say YES.
Another problem to suspect when you have funky cell behavior is if you are correctly handling cell reuse. You didn't show us that code, so we can't tell. If you are keeping a reference to a cell somewhere and setting the image data into that - you'll hose yourself when the table reuses the cell out from under you. (I don't think that's the problem in this case.)

How do I create my own store by subclassing of IASKAbstractSettingsStore?

First off, InAppSettingsKit is just what I was looking for. Once I figure out some things it will save me tons of time.
My question is: how do I create my own store by subclassing IASKAbstractSettingsStore? The home of IASK states:
The default behaviour of IASK is to store the settings in [NSUserDefaults standardUserDefaults]. However, it is possible to change this behaviour by setting the settingsStore property on an IASKAppSettingsViewController.
and
The easiest way to create your own store is to create a subclass of IASKAbstractSettingsStore.
I've spent a good deal of time combing through the code, and I think I understand the basic structure of it. However, I can't figure out how and what to set the settingsStore property to.
I can see the settingsStore defined and implemented in IASKAppSettingsViewController:
id<IASKSettingsStore> _settingsStore;
and
- (id<IASKSettingsStore>)settingsStore {
if (!_settingsStore) {
_settingsStore = [[IASKSettingsStoreUserDefaults alloc] init];
}
return _settingsStore;
}
I tried subclassing IASKAbstractSettingsStore:
#import <Foundation/Foundation.h>
#import "IASKSettingsStore.h"
#interface IASKSettingsStoreMeals : IASKAbstractSettingsStore {
NSString * _filePath;
NSMutableDictionary * _dict;
}
- (id)initWithPath:(NSString*)path;
#end
and then modified IASKAppSettingsViewController's settingsStore property to allocate and initialize my new class IASKSettingsStoreMeals instead of IASKSettingsStoreUserDefaults - the only way I can see to change the property:
- (id<IASKSettingsStore>)settingsStore {
if (!_settingsStore) {
_settingsStore = [[IASKSettingsStoreMeals alloc] init];
}
return _settingsStore;
}
When I build and run, I get the following message when I try the first control (the toggle switch), all other fields do not get saved:
attempt to insert nil value at objects[0] (key: toggleSwitch)
What am I doing wrong? In addition to the changes needed to "rejigger" the code to use IASKSettingsStoreFile (or a subclassed IASKAbstractSettingsStore), I also can't see where to set the file path change the location of where the settings are saved - or is that done behind the scenes. Looking forward to get past this learning curve and using this.
Found the answer.
My question reveals my inexperience with object orientated languages on the whole, and the concept of encapsulation and frameworks in particular. No changes needed to be made to the IASK framework code, all code was added on my root view controller.
I created another instance of IASKAppSettingsViewController, and added the following code to change the plist location:
// the path to write file
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *settingsFile = [documentsDirectory stringByAppendingPathComponent:#"mySettings"];
IASKSettingsStoreFile *mySettingsBundle = [[IASKSettingsStoreFile alloc] initWithPath:settingsFile];
self.appSettingsViewController.settingsStore = mySettingsBundle;
UINavigationController *aNavController = [[UINavigationController alloc] initWithRootViewController:self.appSettingsViewController];
[mySettingsBundle release];
self.appSettingsViewController.settingsStore = mySettingsBundle;
//[viewController setShowCreditsFooter:NO]; // Uncomment to not display InAppSettingsKit credits for creators.
// But we encourage you not to uncomment. Thank you!
self.appSettingsViewController.showDoneButton = YES;
[self presentModalViewController:aNavController animated:YES];
[aNavController release];

Navigating between View controllers?

In my Iphone application I am trying to navigate from one table view controller to next table view controller. Problem I am facing is that I have to fetch data using http request and then parse this data when the user select a cell. I am able to fetch and parse the data but the view controller is not waiting for the data to parsed and the next view controller is shown (which is empty). How to over come this problem.
indexSelected = [NSString stringWithFormat: #"%d",[indexPath row] ];
[[MySingletonClass sharedMySingleton] doAnAuthenticatedAPIFetch_Subscriber_Detail : indexSelected];
SubscribersDetailViews2 *viewController = [[SubscribersDetailViews2 alloc] initWithNibName:#"SubscribersDetailViews2" bundle:nil];
[[self navigationController] pushViewController:viewController animated:YES];
[viewController release];
This is what you do:
indexSelected = [NSString stringWithFormat: #"%d",[indexPath row] ];
SubscribersDetailViews2 *viewController = [[SubscribersDetailViews2 alloc] initWithNibName:#"SubscribersDetailViews2" bundle:nil];
[[MySingletonClass sharedMySingleton] doAnAuthenticatedAPIFetch_Subscriber_Detail:indexSelected delegate:self];
[[self navigationController] pushViewController:viewController animated:YES];
[viewController release];
You define a protocol that your view controller conforms to and when the fetching and parsing of data is done you call a method on the delegate to let the view controller know that the data is ready to be displayed.
If you need more information on how to do this, leave a comment.
EDIT: So here's how to declare and use a protocol. I'm going to try to keep it as simple as possible. I'm not sure if I like your naming convention, but I'll still use it for this example.
So let's get down to the code. This is how you declare a protocol:
#protocol MySingletonClassDelegate <NSObject>
#optional
- (void)didDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithData:(NSArray *)data;
- (void)failedToDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithError:(NSError *)error;
#end
Again, I'm not too fond of the naming convention. You shouldn't have underscores in objective-c method names.
The protocol should be defined in MySingletonClass.h before the declaration of MySingletonClass.
I declared two methods in the protocol, one for delivering the data and one for delivering an error if it fails, so that you can notify the user that it failed.
To use the protocol you need the following:
#interface SubscribersDetailViews2 : UITableViewController <MySingletonClassDelegate>
You also need to implement the methods declared in the protocol, but I'll leave that implementation to you.
Since the fetching of data already seems to be happening in the background I don't think I'll need to explain how to do that. One important thing to remember is that you want to execute the delegate methods on the main thread. Here's the code to do that:
- (void)doAnAuthenticatedAPIFetch_Subscriber_Detail:(NSUInteger)index delegate:id<MySingletonClassDelegate>delegate {
// Fetching data in background
if (successful) {
[self performSelectorOnMainThread:#selector(didDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithData:) withObject:data waitUntilDone:NO];
} else {
[self performSelectorOnMainThread:#selector(failedToDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithError:) withObject:error waitUntilDone:NO];
}
}
Just to be clear the // Fetching data in background is supposed to be replaced by your code. I assume that your code produces the variables (NSArray *data, NSError *error, BOOL successful) that I use.
That's about it, if you need clarification on anything let me know.
There are a number of options:
Cache the data, i.e., take a full copy of it on the iOS device (may not be practical of course)
Display an interstitial screen saying "loading" and then move to the "real" screen when the data has downloaded
Have, effectively, two different data sources for your table. The first is your current one. The second would be a single cell saying "Loading..."
In short, there's no point and click way of doing this but there's no problem downloading the data on the fly as long as you tell your users what's happening.