I have a project that targets both Mac OS X 10.4 and 10.5, where 10.5 is the base SDK.
Some methods like -[NSString stringByReplacingOccurrencesOfString:withString] are unavailable in 10.4. I could just implement the functionality by hand. Another option would be to implement the method as a category, but that would mess with the 10.5 implementation and that's something I'd like to avoid.
So how do I implement such methods in 10.4 without messing up 10.5 and in such a way that I can take out the implementation easily when I decide to stop supporting 10.4?
I think you have to use +load and +initialize to load a method at runtime if the method doesn't already exists.
if ([myString respondsToSelector: #selector(stringByReplacingOccurrencesOfString:withString:)])
{
// 10.5 implementation
}
else
{
// 10.4 implementation
}
Use a category, but put a tag on the method name; for example, stringByReplacingOccurrencesOfString_TigerCompatible:. In the implementation, call either Leopard's implementation or your own.
When you go Leopard-only, do a project search for “TigerCompatible”, then burninate all of those methods and un-tag all of their call sites.
Put all the missing implementation in categories in a bundle which is loaded on startup in main() if running under Tiger.
How about using a C preprocessor macro to insert the relevant methods if it's being built for 10.4? Maybe try doing something like this in a category, so those methods which don't exist on 10.4 are only included if it's being built for 10.4?
#if defined(MAC_OS_X_VERSION_10_4) && MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4
// Put your implementations of the methods here
#endif
Do you need to support 10.4? If you're using 10.5 only methods in core parts of your app then it might be time to consider going 10.5 only.
Anyway, with the specific example given above, I suggest moving away from that and making a mutable copy of your string so you can use the similar method on NSMutableString which does work in 10.4
Related
I am using a 3rd party Obj-C static library for and API in a RubyMotion project and one of the classes in the library defines some public instance variables in the interface section in the API header file. Here is the code as it's pretty short:
#interface TransporterFile : NSObject
{
#public
NSString *name;
TransporterFileType type;
NSDate *modifyTime;
NSDate *createTime;
unsigned long long size;
}
- (BOOL) isFolder;
- (BOOL) isShared;
#end
In ObjC, these values can be reached like this:
name = transporterFile->name
but there is no getter defined so you can't use dot notation.
In RubyMotion, probably because there is no getter, there is no instance variable exposed to me. I have tried using the .instance_variables method and it returns an empty array. I also tried the instance_variable_get('#name') method but that doesn't work either. I am able to call the two methods isFolder and isShared and they work as expected.
Update: I tried doing a similar thing in the Swift language and it had similar problems and that led me to a suggestion to use the valueForKeyPath('name') method to access the 'name' instance variable. That worked in Swift and led me to check if a similar method was available to RubyMotion. The good news is that this works but is a bit clunky so I'll leave this question open for now in the hope that a better answer is available.
I realise the 3rd party library is not following best practice and I will recommend to the vendor that they define some properties instead if using instance variables directly but for now I need a workaround.
Can anyone please suggest a way to access these public instance variables from RubyMotion or perhaps by wrapping the vendor's library in another ObjC library. I've never written an ObjC library (wrapper or not) so would appreciate some advice before I embark on this option.
The full API can be seen here: https://secure.connecteddata.com/developer
Note that I'm using Mac OS X 10.10 with Xcode 6.1.1 and the latest version of RubyMotion. The Mac OS X API download from the above site is missing the header file so I used the header from the iOS download.
Many thanks,
Craig.
I updated my question to explain that I found the following method allows me to access the public instance variables.
obj.valueForKeyPath('name')
However, I'd be happy to hear of any better ways to do this as it's a bit clunky.
ps. Why is it that I can spend hours trying to find a solution to a problem then within minutes of posting the question on an open forum, I find a potential answer? Grr... :)
I'm trying to build a library that has convenience methods for dealing with the iOS and OSX AddressBook frameworks (Contact List on iOS and Contacts on OSX) and includes such methods as:
(BOOL)addressBookContainsRecordWithID:(NSString *)recordID (NSInteger in the case of OSX)
(id)newOrExistingGroupWithName:(NSString *)groupName
(id)addressBookRecordWithID:(NSString *)recordID
etc.
And I'd like to be able to call these methods on both OSX and iOS, but have that method execute the logic path respective for each device. For example, on OSX it would use a method that uses ABPerson and ABGroup whereas on iOS it would use a method that uses ABRecordRef.
My current plan is to have preprocessor directives (#if device-is-osx callOSXMethod #else callIOSMethod) which figure out whether I'm using iOS or OSX and use the correct method.
Is there a better way I could go about this? I feel like there is some design patter that could be used here.
It depends on how much shared code there will be between your two different implementations. If almost all of the code is shared except for a few specific method calls to the AddressBook frameworks then preprocessor directives with #if TARGET_OS_IPHONE are the way to go.
On the other hand, if you find that the code in your #if blocks is getting quite long, it usually makes more sense to split the Mac and iOS implementations into separate files that share an identical interface. Thus you'd have:
MyAddressBook.h - Shared by both
MyAddressBook_Mac.h - Mac implementation of MyAddressBook
MyAddressBook_iOS.h - iOS implementation of MyAddressBook
This is made easier by the fact that your implementations can even have different instance variables by using a class extension inside each implementation file. This second pattern leads to greater readability at the cost of having to maintain whatever common code there is in two places.
Preprocessor directives are the way to go. There are even predefined ones you can use:
#if TARGET_OS_IPHONE
// iOS code here
#else
// OS X code here
#endif
We have a NSUUID class (we provide the declaration and implementation). We used it successfully up to iOS 6.0. We implemented it because UIDevice uniqueIdentifier was banned long before Apple deprecated it, and returning a NSUUID was a natural choice.
At iOS 6.0, we had to guard the define because Apple introduced the same class:
#if __IPHONE_OS_VERSION_MAX_ALLOWED <= __IPHONE_5_1
#interface NSUUID : NSObject {
...
}
#endif
iOS 5.1 and lesser are now broken. On iOS 5.1, we get back nil after alloc/init.
I tried to remove the #if/#end, but I get duplicate names when using the latest iPhone SDK.
Apple's lack of a stable API is a bug, not a feature. This "try it at runtime" crap is not cutting it. It makes it very difficult to write high integrity software.
From Tommy's response below, I can't instruct Apple's toolchain to use our implementation of NSUUID all the time. How do I provide an implementation of NSUUID for iOS 5.1 and lower (that might be compiled using the latest SDK)?
You can't. You've explicitly broken the rules:
Objective-C classes must be named uniquely [...] In order to keep
class names unique, the convention is to use prefixes on all classes.
You’ll have noticed that Cocoa and Cocoa Touch class names typically
start either with NS or UI. Two-letter prefixes like these are
reserved by Apple for use in framework classes.
You'll need to rename your own class. The quickest way is quite probably to right click on the class name, select "Refactor -> Rename..." and use a correct prefix this time. Xcode may not be able to refactor fully automatically since it'll obviously be ambiguous which NSUUID you're referring to in other parts of your code.
EDIT: regardless of grandstanding, if you want to implement code that provides a self-implemented replacement for NSUUID where it's not available then the solution is to "try it at runtime".
Assuming you've implemented NDRUUID, which implements the same interface as NSUUID then the quickest solution is to add something like this to your prefix header:
#define NSUUID (NSClassFromString(#"NSUUID") ? [NSUUID class] : [NDRUUID class])
You can then use [NSUUID UUID], etc, everywhere else in your code as though you were targeting iOS 6 only; the only difference is that when running under 5 you'll actually be addressing NDRUUID. Whenever you stop supporting 5 just remove that line from your prefix header and delete your own class from the project.
Hopefully you can see this is a much better way to handle introducing new APIs and backwards compatibility than, say, not using Apple's NSUUID at all anywhere until it's available everywhere.
I have built exactly what you asked for: an implementation of NSUUID for iOS 5.1 and lower that might be compiled using the latest SDK. See my NSUUID project on GitHub.
As far as I understand when you use properties the compiler still converts them to accessor methods during compilation. I got a little irritated when I read you need OSX 10.5 or later to use properties. Why is that so?
If in the compiled application are in fact still accessor methods I see no need for OSX 10.5. Or is there something else going on during run-time?
Because the Objective-C 2.0 runtime was not back ported to 10.4. You need compiler and runtime support to handle all of ObjC 2.0 properly.
Is there a way to provide a method implementation (that bears the exact same name of a method defined by the framework) only when the method isn't already defined in the system? For example method [NSSomeClass someMethod:] exists only in Mac OS X 10.6 and if my app runs in 10.5, I will provide that method's definition in a category. But when the app runs in 10.6, I want the OS-provided method to run.
Background: I'm creating an app targeted for both 10.5 and 10.6. The problem is that I recently realized that method +[NSSortDescriptor sortDescriptorWithKey:ascending:] only exists in 10.6 and my code is already littered by that method call. I could provide a default implementation for it (since this time it's not too difficult to implement it myself), but I want the "native" one to be called whenever my app runs on 10.6. Furthermore if I encounter similar problems in the future (with more difficult-to-implement-myself methods), I might not be able to get away with providing a one-liner replacement.
This question vaguely similar to Override a method via ObjC Category and call the default implementation? but the difference is that I want to provide implementations only when the system doesn't already have one.
Thanks.
I would compile +[NSSortDescriptor sortDescriptorWithKey:ascending:] in a category into a separate bundle. Then at the very beginning of your main, check if the NSSortDescriptor class has the sortDescriptorWithKey:ascending: method with respondsToSelector:. If it's not implemented (i.e. you are running on < 10.6), then load the bundle with -[NSBundle loadAndReturnError:].
This way, you will run the OS-provided method on 10.6 and your implementation on 10.5.
Yes, this is possible. Since you are targetting 10.5+ I'm assuming you are using the ObjC2 runtime, which makes it fairly straightforward.
The Objective-C Runtime Reference has all the methods you will need. Specifically, you can use class_getClassMethod or class_getInstanceMethod to see if the method already exists, and then class_addMethod to bind your implementation to that selector if the class doesn't already have it.
Alternatively, you could just do a find and sub -[NSSortDescriptor initWithKey:ascending:] then add the appropriate release statement.
It's more cumbersome to implement but far less fragile and error prone than altering the class itself. That is especially true if you've never done that before. You'll probably spend more time coming up to speed on the override than you would just doing the find.
think about compromise between changing initWithKey:ascending: method and and adding new method at runtime - just subclass NSSortDescriptor and replace all NSSortDescriptor calls with NSMySortDescriptor;
//NSMySortDescriptor.h
#interface NSMySortDescriptor : NSSortDescriptor {
}
- (id)initWithKey:(NSString *)keyPath ascending:(BOOL)ascending
#end
//NSMySortDescriptor.m
#implementation NSMySortDescriptor
- (id)initWithKey:(NSString *)keyPath ascending:(BOOL)ascending{
// check if super i.e. has initWithKey:ascending: method
if( [NSSortDescriptor instancesRespondToSelector:#selector(initWithKey:ascending:)] ) {
[NSSortDescriptor initWithKey:ascending:];
}
else{
// your custom realization for Mac OS X 10.5
//...
}
}
#end