Handling private frameworks in Xcode ≥ 7.3
Solution 1:
You can solve this problem by linking to the private framework dynamically, instead of the more common way of linking at build time. At build time, the BluetoothManager.framework would need to exist on your development Mac for the linker to be able to use it. With dynamic linking, you defer the process until runtime. On the device, iOS 9.3 still has that framework present (and the other ones, too, of course).
Here is how you can modify your project on Github:
1) In Xcode's Project Navigator, under the Frameworks, remove the reference to BluetoothManager.framework. It was probably showing in red (not found) anyway.
2) Under the project Build Settings, you have the old private framework directory explicitly listed as a framework search path. Remove that. Search for "PrivateFrameworks" in the build settings if you have trouble finding it.
3) Make sure to add the actual headers you need, so the compiler understands these private classes. I believe you can get current headers here for example. Even if the frameworks are removed from the Mac SDKs, I believe this person has used a tool like Runtime Browser on the device to generate the header files. In your case, add BluetoothManager.h and BluetoothDevice.h headers to the Xcode project.
3a) Note: the generated headers sometimes don't compile. I had to comment out a couple struct
typedefs in the above Runtime Browser headers in order to get the project to build. Hattip @Alan_s below.
4) Change your imports from:
#import <BluetoothManager/BluetoothManager.h>
to
#import "BluetoothManager.h"
5) Where you use the private class, you're going to need to first open up the framework dynamically. To do this, use (in MDBluetoothManager.m):
#import <dlfcn.h>
static void *libHandle;
// A CONVENIENCE FUNCTION FOR INSTANTIATING THIS CLASS DYNAMICALLY
+ (BluetoothManager*) bluetoothManagerSharedInstance {
Class bm = NSClassFromString(@"BluetoothManager");
return [bm sharedInstance];
}
+ (MDBluetoothManager*)sharedInstance
{
static MDBluetoothManager* bluetoothManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// ADDED CODE BELOW
libHandle = dlopen("/System/Library/PrivateFrameworks/BluetoothManager.framework/BluetoothManager", RTLD_NOW);
BluetoothManager* bm = [MDBluetoothManager bluetoothManagerSharedInstance];
// ADDED CODE ABOVE
bluetoothManager = [[MDBluetoothManager alloc] init];
});
return bluetoothManager;
}
I placed the call to dlopen
in your singleton method, but you could put it elsewhere. It just needs to be called before any code uses the private API classes.
I added a convenience method [MDBluetoothManager bluetoothManagerSharedInstance]
because you'll be calling that repeatedly. I'm sure you could find alternate implementations, of course. The important detail is that this new method dynamically instantiates the private class using NSClassFromString()
.
6) Everywhere you were directly calling [BluetoothManager sharedInstance]
, replace it with the new [MDBluetoothManager bluetoothManagerSharedInstance]
call.
I tested this with Xcode 7.3 / iOS 9.3 SDK and your project runs fine on my iPhone.
Update
Since there seems to be some confusion, this same technique (and exact code) still works in iOS 10.0-11.1 (as of this writing).
Also, another option to force loading of a framework is to use [NSBundle bundleWithPath:]
instead of dlopen()
. Notice the slight difference in paths, though:
handle = dlopen("/System/Library/PrivateFrameworks/BluetoothManager.framework/BluetoothManager", RTLD_NOW);
NSBundle *bt = [NSBundle bundleWithPath: @"/System/Library/PrivateFrameworks/BluetoothManager.framework"];