I'm looking to make my app compatible with older versions of iPhone OS. I did see weak linking mentioned as an option. Can I use OS version detection code to avoid code blocks that the OS can't handle? (Say iAD?)
if(OS >= 4.0){
//set up iADs using "NDA code"...
}
If yes, what goes in place of if(OS >= 4.0)?
You should be weak linking against the new frameworks. Alongside that you should be checking the availability of new APIs using methods like NSClassFromString, respondsToSelector, instancesRespondToSelector etc.
Eg. Weak linking against MessageUI.framework (an old example, but still relevant)
First check if the MFMailComposerController class exists:
Class mailComposerClass = NSClassFromString(#"MFMailComposerController");
if (mailComposerClass != nil)
{
// class exists, you can use it
}
else
{
// class doesn't exist, work around for older OS
}
If you need to use new constants, types or functions, you can do something like:
if (&UIApplicationWillEnterBackgroundNotification != nil)
{
// go ahead and use it
}
If you need to know if you can use anew methods on an already existing class, you can do:
if ([existingInstance respondsToSelector:#selector(someSelector)])
{
// method exists
}
And so on. Hope this helps.
Related
I have an app that was originally developed in Swift 2 / Xcode 7.3.1 using the Firebase iOS SDK v2.5.1 which is currently being updated to the latest versions of Swift/Xcode, which entails also updating the Firebase SDK.
We have hundreds of unit tests that were developed that, for the most part have been easy to update, but there are a few in particular that are tricky due to the way the old Firebase SDK was structured (and thus how it was used).
Originally, because nearly all Firebase database functionality was encompassed in one "Firebase" class, we created a custom class that inherited from the Firebase class that overrode several methods. In the latest SDK, functionality has been split further into different classes (namely the separate of Database and DatabaseReference), and most of the methods that were overridden are now a part of the DatabaseReference class in the FirebaseDatabase framework.
To make this a bit more tangible.
Old code:
class FirebaseTest: Firebase {
override func childByAutoId() {
//generate and save Auto ID internally to be used later for testing/validation
super.childByAutoID();
}
//many other overridden methods
}
//Unit tests
let fbref = FirebaseTest(url: "my-firebase.firebaseio.com/test");
// etc
The old Firebase SDK would return a Firebase reference to the base URL provided in the constructor, and because many of the properties we used were members of Firebase, all of our testing worked.
Following the SDK migration guide, I end up with the following code (simply replacing Firebase with DatabaseReference):
class FirebaseTest: DatabaseReference {
override func childByAutoId() {
//generate and save Auto ID internally to be used later for testing/validation
super.childByAutoID();
}
//many other overridden methods
}
//Unit tests
let fbref = FirebaseTest(fromUrl: "my-firebase.firebaseio.com/test")
// etc
Now, immediately, there is a problem with the above code that prevents it from compiling: the DatabaseReference constructor does not take any arguments. I can change it to the following
let fbref = FirebaseTest();
but now I know that, while it will compile, the tests will absolutely fail. The tricky part is that, in the latest SDK, database references are only supposed to be retrieved from a Database as follows:
//don't do this!
let fbref = DatabaseReference(); //will compile, but will break at runtime. Upon inspection in the debugger, there are several internal properties that should be set that are null. Those internal properties are likely all set correctly if the database reference is obtained correctly
//do it one of these ways
let fbref2 = Database.database().reference();
let fbref3 = Database.database().reference(fromUrl: "my-firebase.firebaseio.com/test") //also have access to this "reference(fromUrl: )" method from Database.database()
let fbref4 = Database.database().reference(withPath: "/test") //also have access to this "reference(withPath: )" method from Database.database()
So, hopefully my conundrum is becoming clear -- because I can only retrieve a correctly-initalized DatabaseReference from a Database, rather than my version of DatabaseReference that has overridden methods for testing, I'm fairly stuck.
Also, I have looked into creating an extension of Database itself as follows:
class FirebaseTest: DatabaseReference {
override func childByAutoId() {
//generate and save Auto ID internally to be used later for testing/validation
super.childByAutoID();
}
//many other overridden methods
}
class FirebaseDatabaseTest: Database {
override func reference(fromUrl: String) {
return FirebaseTest();
}
}
let fbref = FirebaseDatabaseTest().reference(fromUrl: "my-firebase.firebaseio.com/test")
But, even though I am intercepting the url in the overridden reference method, there's no clear way for me to "set up" the DatabaseReference the correct way using that URL.
Thoughts?
I'm trying to get an example of running OpenEars with the RapidEars plugin running in Swift 2.2 (XCode 7.3.1). However, I suspect I'm having a larger issue with using Objective-C interfaces with extensions in a Swift project (or my understanding of how that works).
The OpenEars code is Obj-C. However I was able to get it running in my swift project through the standard Obj-C -> Swift translation techniques.
Abbreviated code follows. The full example is on a forked Github and updated to Swift-2.2: https://github.com/SuperTango/OpenEars-with-Swift-
This following example is working great. You can see the entire project by checkng out the "working-opears-swift2.2" tag.
OpenEarsTest-Bridging-Header.h:
#import <OpenEars/OELanguageModelGenerator.h>
#import <OpenEars/OEAcousticModel.h>
#import <OpenEars/OEPocketsphinxController.h>
#import <OpenEars/OEAcousticModel.h>
#import <OpenEars/OEEventsObserver.h>
ViewController.swift:
class ViewController: UIViewController, OEEventsObserverDelegate {
var openEarsEventsObserver = OEEventsObserver()
override func viewDidLoad() {
super.viewDidLoad()
loadOpenEars()
}
func loadOpenEars() {
self.openEarsEventsObserver = OEEventsObserver()
self.openEarsEventsObserver.delegate = self
var lmGenerator: OELanguageModelGenerator = OELanguageModelGenerator()
addWords()
var name = "LanguageModelFileStarSaver"
lmGenerator.generateLanguageModelFromArray(words, withFilesNamed: name, forAcousticModelAtPath: OEAcousticModel.pathToModel("AcousticModelEnglish"))
lmPath = lmGenerator.pathToSuccessfullyGeneratedLanguageModelWithRequestedName(name)
dicPath = lmGenerator.pathToSuccessfullyGeneratedDictionaryWithRequestedName(name)
}
func startListening() {
do {
try OEPocketsphinxController.sharedInstance().setActive(true)
OEPocketsphinxController.sharedInstance().startListeningWithLanguageModelAtPath(lmPath, dictionaryAtPath: dicPath, acousticModelAtPath: OEAcousticModel.pathToModel("AcousticModelEnglish"), languageModelIsJSGF: false)
} catch {
NSLog("Error!")
}
}
// A whole bunch more OEEventsObserverDelegate methods that are all working fine...
func pocketsphinxDidStartListening() {
print("Pocketsphinx is now listening.")
statusTextView.text = "Pocketsphinx is now listening."
}
Up until this point, everything is working great.
However, In order to use the "RapidEars" plugin, the documentation (http://www.politepix.com/rapidears/) says to:
Add the framework to the project and ensure it's being included properly.
import two new files (that are both "categories" to existing OpenEars classes):
#import <RapidEarsDemo/OEEventsObserver+RapidEars.h>
#import <RapidEarsDemo/OEPocketsphinxController+RapidEars.h>
Change methods that used: startListeningWithLanguageModelAtPath to use startRealtimeListeningWithLanguageModelAtPath
add two new OEEventsObservableDelegate methods.
func rapidEarsDidReceiveLiveSpeechHypothesis(hypothesis: String!, recognitionScore: String!)
func rapidEarsDidReceiveFinishedSpeechHypothesis(hypothesis: String!, recognitionScore: String!)
The new code can be found by checking out the rapidears-notworking-stackoverflow tag from the above github repo
Problem 1:
When doing completion in the XCode editor, the editor sees WILL perform autocompletion on the startRealtimeListeningWithLanguageModelAtPath method, however when the code is run, it always fails with the error:
[OEPocketsphinxController startRealtimeListeningWithLanguageModelAtPath:dictionaryAtPath:acousticModelAtPath:]: unrecognized selector sent to instance 0x7fa27a7310e0
Problem 2:
When doing auto completion in the XCode editor, it doesn't see the two new delegate methods defined in RapidEarsDemo/OEPocketsphinxController+RapidEars.h.
I have a feeling that these are related, and also related to the fact that they failing methods are defined as Categories to Objective-C classes. But that's only a guess at this point.
I've made sure that the RapidEars framework is imported and in the framework search path.
Can anyone tell me why this is happening? Or if there's some Swift magic incantation that I missed?
The problem could be the one described in the link below, where category methods in a static library produce selector not recognized runtime errors.
Technical Q&A QA1490: Building Objective-C static libraries with categories
I am developing an app using SDK 8.1, Apple LLVM 6.0 and Xcode 6.1.1. The deployment target is 6.0. I'm using NSOperationQueue and I want to use QoS whenever it's available.
The code I'm using is:
if ([self.operationQueue respondsToSelector:#selector(setQualityOfService:)]
&& (&NSOperationQualityOfServiceUserInitiated)) {
[self.operationQueue performSelector:#selector(setQualityOfService:) withObject: NSOperationQualityOfServiceUserInitiated];
} else {
//Other stuff not related to the scope of this question
}
The error I get is:
Use of undeclared identifier 'NSOperationQualityOfServiceUserInitiated'
I added the if (&NSOperationQualityOfServiceUserInitiated) part to check if this constant exists. This code worked with older versions of Xcode/Obj-C Compiler.
I am able to use selectors with performSelectorWithIdentifier but what about constants that do not have a defined value in the docs? The value of this constant is set by NSQualityOfServiceUserInitiated but there is no definition for this value that can be hardcoded.
How do I fix that?
There are several things wrong with the code.
NSOperationQualityOfServiceUserInitiated is a native type (NSInteger) so you can't use it the way that you are in either line.
The qualityOfService is a property of type NSQualityOfService. Your attempt to pass an argument to the qualityOfService method (getter method) makes no sense. If you are trying to set the quality of service, you need to call the setter but you can't use performSelector.
You want:
if ([self.operationQueue respondsToSelector:#selector(qualityOfService)]) {
self.operationQueue.qualityOfService = NSOperationQualityOfServiceUserInitiated;
} else {
//Other stuff not related to the scope of this question
}
This code will compile fine as long as your Base SDK is iOS 8.0 or later. The Deployment Target doesn't matter.
If you also want to build this code with Xcode 5 or earlier (a Base SDK of iOS 7 or earlier) then you need to wrap the code with the proper compiler directives to check for the Base SDK.
I am using ECSlidingViewController for "hamburger" menu. I am using SDK 7.0 but I changed deployment target to iOS 6.1 and now I am trying my app with older iOS then 7. The problem is with setEdgesForExtendedLayout. There is older ECSlidingViewController for older system versions. So my question is how can I change to use old version for iOS 6.1 and older and newer version for iOS 7.0 and newer. I include files from both ECSlidingViewController projects (not by cocoapods but if it is need then it's not problem to change it). I guess I need check for OS version and then change imports but I am not sure if it is enough and what's best name convention for both project. I guess they should be in different folders (like ECSlidingViewController and ECSlidingViewControllerOld) but class should be same name, is it right?
Edit: Example of code with edgesForExtendedLayout:
- (CGRect)underLeftViewCalculatedFrameForTopViewPosition:(ECSlidingViewControllerTopViewPosition)position {
CGRect frameFromDelegate = [self frameFromDelegateForViewController:self.underLeftViewController
topViewPosition:position];
if (!CGRectIsInfinite(frameFromDelegate)) return frameFromDelegate;
CGRect containerViewFrame = self.view.bounds;
if (!(self.underLeftViewController.edgesForExtendedLayout & UIRectEdgeTop)) {
CGFloat topLayoutGuideLength = [self.topLayoutGuide length];
containerViewFrame.origin.y = topLayoutGuideLength;
containerViewFrame.size.height -= topLayoutGuideLength;
}
if (!(self.underLeftViewController.edgesForExtendedLayout & UIRectEdgeBottom)) {
CGFloat bottomLayoutGuideLength = [self.bottomLayoutGuide length];
containerViewFrame.size.height -= bottomLayoutGuideLength;
}
if (!(self.underLeftViewController.edgesForExtendedLayout & UIRectEdgeRight)) {
containerViewFrame.size.width = self.anchorRightRevealAmount;
}
return containerViewFrame;
}
I am not a fan of including duplicate versions of libraries, as this creates a big problem with naming, and a lot of work to refactor all old classes to have some *-OLD suffix. Since you have access to the source, you can modify the newer version like so:
if(NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1)
{
[vc setEdgesForExtendedLayout:UIRectEdgeNone];
//Any other iOS7-specific code.
}
First, the easiest way to do it will be have the most recent version of component that support the lowest deployment target.
But if you really want to have different version for each iOS, I don't know better solution, than just rename all classes from, for example, older version (because there was only two classes) and manage creation of this controller programmatically, because there's no way to set different class for different iOS version in xib's or storyboard's. You need to wrap each and every call of this component with iOS version check ( How to check iOS version? ).
Imports and variables you can leave for each version of component without check.
The trick you mentioned failed in this case, because it works good for different architecture, because binaries for different architectures will be included in final app and that's named fat binary, but there's the same architecture for iOS6 and iOS7 ( only one new in iOS7 - arm64). So you can't wrap only includes with preprocessor macros and get fat binary with different code for each iOS version.
I hope you've understood something from my explanation.
It seems that the only thing preventing you from using the new version with iOS 6 is the lack of topLayoutGuide and bottomLayoutGuide, which if available would just return lengths of zero anyway.
Why not "backport" those methods in your controller?
- (id<UILayoutSupport>)topLayoutGuide
{
CustomLayoutGuide * guide = [CustomLayoutGuide new];
guide.length = SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0") ? super.topLayoutGuide.length : 0.0;
return guide;
}
And the helper class
#interface CustomLayoutGuide : NSObject <UILayoutSupport>
#property(nonatomic) CGFloat length;
#end
#implementation CustomLayoutGuide
#end
IMHO the easiest thing to do (and probably the best though) is to take the current ECSlidingViewController and integrate part of the old when edgesForExtendedLayout is not available (e.g. if ([self respondsToSelector:#selector(edgesForExtendedLayout)]) { /*...*/} ). This should be pretty easy and fast to do since the old ECSlidingViewController just consists of two files and these files are also present in the new ECSlidingViewController.
You could make your life even easier when doing this by using a diff tool to be sure of what you should add.
I am developing an application that I want to run on both iOS4 and iOS5 but for users of iOS5 I want to use an iOS5 feature as part of the interface (iOS4 users will get something less exciting). My question is what is the procedure for checking the availability of a particular OS on a device. My understanding is that I don't check the OS version but rather the availability of a particular class, can anyone help me out of the best way to do this ...
Isn't iOS 5 under NDA?
Anyway, to check if a feature exists then try this:
if (NSClassFromString(#"UIStepper")) {
//enter code here
} else {
//enter code here
}
Customise to your needs.
EDIT: iOS 5 is now released so I can now add "UIStepper" to my code.
iOS5 is under NDA so i wouldnt mention any new classes that may or may not exist. However the following code should do what you want. It's lifted from the docs.
if ([UINewClass class]) {
// Create an instance of the class and use it.
} else {
// Alternate code path to follow when the
// class is not available.
}
This uses Weak Linking and therefore requires that the new class (UINewClass) to be in the SDK you are using to compile. It is a relatively new feature introduced in iOS 4.2 and might not be supported by all the frameworks. A workaround is to use the older style (from the same link as above):
Class cls = NSClassFromString (#"UINewClass");
if (cls) {
// Create an instance of the class and use it.
} else {
// Alternate code path to follow when the
// class is not available.
}
Class stepperClass = NSClassFromString(#"UIStepper");
if (stepperClass) {
// class is available, use it
} else {
// class not available, don't use it or use something else
}
If you're trying to get the UIStepper to gracefully degrade in 4.x, you cannot only use
if( NSClassFromString(#"UIStepper") )
Instead, you must also check for a UIStepper-specific selector having a response. As Harry Wood suggested in the comment under Bo A, a good way to do it is:
if( NSClassFromString(#"UIStepper") && [theStepper respondsToSelector:#selector(setValue:)] )
This solved the issue of my app crashing under iOS 4.x.
Harry Wood helped me solve the issue I was seeing, and I would like him to get the credit.