How to use a class variable? - objective-c

I have a class method loadImage:(NSString *)pathto load image from path, if the path is nil, the load the default path image.
+(NSImage *) loadImage:(NSString *)path{
if(path== nil){
path = [[NSBundle mainBundle] pathForResource:#"default" ofType:#"png"];
}
}
because the default path is always using the same path, I want to calculate the path only once if I run the method 1000 times, like
if(defaultPath == nil){
defaultPath = [[NSBundle mainBundle] pathForResource:#"default" ofType:#"png"];
}
path = defaultPath;
, I think I can use static variable, but I don't know how to do it, please help me, I'm glad to know any suggestions about improving performance.

Simply use a static variable to hold the default path:
static NSString *defaultPath = …;
if (path == nil) {
path = defaultPath;
}
But if you're doing it for performance reasons, first make sure it's worth it. Most probably it's a premature optimization that's not worth the trouble.

There is a useful pattern based on GCD which handles this sort of situation nicely:
+(NSImage *) loadImage:(NSString *)path{
static NSString *storedPath;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
storedPath = [[NSBundle mainBundle] pathForResource:#"default" ofType:#"png"];
});
// Do whatever you need to do with the default resource path.
}
dispatch_once does exactly what you are after - it ensures that the initialization code is run only once. It is also thread safe.
Xcode's code completion even helps you use the pattern - if you start typing dispatch_once then you'll get the onceToken template directly.

Related

Initialize static NSString at class level

I have a NSString that should be constant in my class. I used the following code to accomplish this:
#interface DefinitionViewController ()
#end
static NSString *preamble;
#implementation DefinitionViewController {
}
+(void)initialize {
if (self == [DefinitionViewController class]) {
NSString *path = [[NSBundle mainBundle] pathForResource:#"preamble" ofType:#"html"];
preamble = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding
error:nil];
}
}
It seems to work fine. I worry about using a file read inside an initialize. Is there a more appropriate means to accomplish the same goal (shared static string)? I could bury this inside my code, but it was much easier to maintain the somewhat large string as an external file.
Thanks for any advice.
"I worry about using a file read inside an initialize".
Don't (worry). The fact that it is, for example, a class method is utterly irrelevant. It is code. It runs and does its job. It is sound code, it runs coherently at a coherent time, and your app bundle is a real thing that really contains the resource. There's no problem here.
If you want to postpone creation of the string, and make assurance doubly sure that the string is not initialized twice, you could instead use a singleton pattern so that the string value is not generated until the first time it is explicitly requested:
+ (NSString*) preamble {
static NSString* preamble = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *path = [[NSBundle mainBundle] pathForResource:#"preamble" ofType:#"html"];
preamble = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
});
return preamble;
}
But there is no special need for this. (EDIT: But see, to the contrary, the comment below of #bbum, who Really Knows Stuff.)

NSURL baseURL returns nil. Any other way to get the actual base URL

I think I don't understand the concept of "baseURL". This:
NSLog(#"BASE URL: %# %#", [NSURL URLWithString:#"http://www.google.es"], [[NSURL URLWithString:#"http://www.google.es"] baseURL]);
Prints this:
BASE URL: http://www.google.es (null)
And of course, in the Apple docs I read this:
Return Value
The base URL of the receiver. If the receiver is an absolute URL, returns nil.
I'd like to get from this example URL:
https://www.google.es/search?q=uiviewcontroller&aq=f&oq=uiviewcontroller&sourceid=chrome&ie=UTF-8
This base URL
https://www.google.es
My question is simple. Is there any cleaner way of getting the actual base URL without concatenating the scheme and the hostname? I mean, what's the purpose of base URL then?
-baseURL is a concept purely of NSURL/CFURL rather than URLs in general. If you did this:
[NSURL URLWithString:#"search?q=uiviewcontroller"
relativeToURL:[NSURL URLWithString:#"https://www.google.es/"]];
then baseURL would be https://www.google.es/. In short, baseURL is only populated if the NSURL is created using a method that explicitly passes in a base URL. The main purpose of this feature is to handle relative URL strings such as might be found in the source of a typical web page.
What you're after instead, is to take an arbitrary URL and strip it back to just the host portion. The easiest way I know to do this is a little cunning:
NSURL *aURL = [NSURL URLWithString:#"https://www.google.es/search?q=uiviewcontroller"];
NSURL *hostURL = [[NSURL URLWithString:#"/" relativeToURL:aURL] absoluteURL];
This will give a hostURL of https://www.google.es/
I have such a method published as -[NSURL ks_hostURL] as part of KSFileUtilities (scroll down the readme to find it documented)
If you want purely the host and not anything like scheme/port etc. then -[NSURL host] is your method.
Docs for BaseURL.
baseURL
Returns the base URL of the receiver.
- (NSURL *)baseURL
Return Value
The base URL of the receiver. If the receiver is an absolute URL, returns nil.
Availability
Available in iOS 2.0 and later.
Declared In
NSURL.h
Seems it only works for relative URLs.
You could possibly use ...
NSArray *pathComponents = [url pathComponents]
and then take the bits you want.
Or try...
NSString *host = [url host];
you can use host method
NSURL *url = [[NSURL alloc] initWithString:#"http://www.hello.com"];
NSLog(#"Host:%#", url.host);
result:
Host:www.hello.com
It might be just me, but when I think further about the double-URL solution, it sounds like something that could stop working between OS updates. So I decided to share yet another solution, definitely not beautiful either, but I find it more readable by the general public since it doesn't rely on any hidden specificity of the framework.
if let path = URL(string: resourceURI)?.path {
let baseURL = URL(string: resourceURI.replacingOccurrences(of: path, with: ""))
...
}
Here's a quick, easy, and safe way to fetch the base URL:
NSError *regexError = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"http://.*/" options:NSRegularExpressionCaseInsensitive error:&regexError];
if (regexError) {
NSLog(#"regexError: %#", regexError);
return nil;
}
NSTextCheckingResult *match = [regex firstMatchInString:url.absoluteString options:0 range:NSMakeRange(0, url.absoluteString.length)];
NSString *baseURL = [url.absoluteString substringWithRange:match.range];

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!

iOS/iPhone SDK: initWithCoder and encodeWithCoder not being called

I'm trying to save an NSMutableArray called queueArray so it can be loaded again after the app has been quit. I used a few tutorials to get me going and this is the code I have come up with. The problem seems to be that "initWithCoder" and "encodeWithCoder" are not being called, shown by no NSLog calls and no stopping at breakpoints. I have added the NSCoding protocol to the .h file and I know that queueArray is not nil and it contains MPMediaItems. Here is some of the code I use to try to save and load the array:
-(IBAction)saveQueuePressed {
NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *filePath = [rootPath stringByAppendingPathComponent:#"queueArray.archive"];
//should cause encodeWithCoder to be called
[NSKeyedArchiver archiveRootObject:queueArray toFile:filePath];
}
-(IBAction)loadQueuePressed {
NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *filePath = [rootPath stringByAppendingPathComponent:#"queueArray.archive"];
//should cause initWithCoder to be called
queueArray = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
}
-(void)encodeWithCoder:(NSCoder *)coder {
NSLog(#"encodeWithCoder");
[coder encodeObject:queueArray forKey:#"savedQueueArray"];
}
-(id)initWithCoder:(NSCoder *)decoder {
NSLog(#"initWithCoder");
queueArray = [decoder decodeObjectForKey:#"savedQueueArray"];
return self;
}
Any help will be greatly appreciated!
The encodeWithCoder: and initWithCoder methods are called when you archive/unarchive an object that responds to them. From what I understand, you have those methods in your class, but the object you are actually archiving (queueArray) is not an instance of that class, it's an NSMutableArray.
If you do want to save your entire object, you can change your saveQueue method to this
-(IBAction)saveQueuePressed {
NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *filePath = [rootPath stringByAppendingPathComponent:#"queueArray.archive"];
// saving the array shouldn't, but saving the self object
//should cause encodeWithCoder to be called:
[NSKeyedArchiver archiveRootObject:self toFile:filePath];
}
But if you just want to save the array, I guess you can just use saveQueuePressed and loadQueuePressed, I don't think you need the encode/init WithCoder: methods
Update:
Maybe your path is not right.
Try
NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *filePath = [[rootPath stringByAppendingPathComponent:#"queueArray.archive"] stringByExpandingTildeInPath]];
Filipe is right! Your comment said you still didn't use his method.
I had this issue too. Switching from the dictionary's atomic write method to the keyedArchiver fixed it, luckily I only had to change one thing, the line that said writeToFile: is now the archive function.
Now my program's working. For some reason, even when responding to NSCoding, the custom object is not being encoded and breaks my dictionary. Is this a bug with iOS? I've read a fair number of Apple Manuals, but I've also seen a fair number of typos and missing info (For example, try MKMapRect functions without the videos to explain them), or Core Animations referencing the Run Loop before you learn threading, I could go on, half finished sentences in Quartz... so yeah, I've read the manuals and this perplexes me, we have to get a more open iOS SDK at some point, hopefully

Objective-C: initWithContentsOfURL returning null

I'm working on an app that would display a remote html and use local images, so what I'm trying to do is download the HTML of the website and display it as a "local" page, that would have access to images in my bundle.
For some reason I can't get initWithContentsOfURL to work. I checked all manuals and examples I could find and it seems that I'm doing it correctly, but the thing just won't work, returns null all the time. The same page loaded with NSURLRequest requestWithURL works fine.
Here is the code:
- (void)awakeFromNib
{
appURL = #"http://dragontest.fantasy-fan.org";
notConnectedHTML = #"Could not connect.";
NSString *seedString = [[NSString alloc] initWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#/seed.php", appURL]]];
NSString *HTMLdata = #"";
if (seedString = #"(null)") {
NSLog(#"Can't connect on awakeFromNib.");
HTMLdata = notConnectedHTML;
}else {
HTMLdata = [NSString stringWithFormat:#"<body style='padding:0px;margin:0px;'>%#%#</body>", seedString, #"<br><img src='images/Default.png'>"];
}
[homeView loadHTMLString:HTMLdata baseURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] resourcePath]]];
}
Firstly, why aren't appURL and notConnectedHTML declared as NSString *? Are they declared this way elsewhere?
Secondly, you might be better off using NSURL's -urlWithString:relativeToURL: to create the actual request URL.
Thirdly (and this is your actual problem here I suspect), to compare two C primitives, you use ==. = is the assignment operator (it makes the thing on the left equal to the thing on the right). To compare two Objective-C objects, use a comparison method, like -isEqual: or -isEqualToString: (which is specifically for NSStrings).
So instead of:
if (seedString = #"(null)")
You should use
if ([seedString isEqualToString:#"(null)"])
However I suspect the reason you're trying to compare to "(null)" is because that's what NSLog spits out when an object is equal to nil. When an object is nil, the object reference itself is equal to the nil constant, so you should use this to see if an object is nil:
if (seedString == nil)
Just for good measure, some people like to use this syntax which does exactly the same thing:
if (!seedString)