-1

I want to load and manipulate SKUIImageColorAnalyzer and SKUIAnalyzedImageColors objects from the private StoreKitUI.framework.

First, I attempt to load the framework at runtime:

guard case let libHandle = dlopen("/System/Library/PrivateFrameworks/StoreKitUI.framework/StoreKitUI", RTLD_NOW) where libHandle != nil else {
    fatalError("StoreKitUI not found")
}

Then, I verify that the SKUIImageColorAnalyzer class can be found:

guard let analyzerClass: AnyClass = NSClassFromString("SKUIImageColorAnalyzer")  else {
    fatalError("SKUIImageColorAnalyzer lookup failed")
}

I want to use the analyzeImage: class method on SKUIImageColorAnalyzer, which takes in a UIImage for analysis and returns an SKUIAnalyzedImageColors object. I do this by verifying the analyzeImage: selector exists on the SKUIImageColorAnalyzer object, and recreate the function:

let selector: Selector = "analyzeImage:"
guard case let method = class_getClassMethod(analyzerClass, selector) where method != nil else {
    fatalError("failed to look up \(selector)")
}

// recreate the method's implementation function
typealias Prototype = @convention(c) (AnyClass, Selector, UIImage) -> AnyObject? // returns an SKUIAnalyzedImageColors object
let opaqueIMP = method_getImplementation(method)
let function = unsafeBitCast(opaqueIMP, Prototype.self)

Now, I can get a UIImage object and pass that in as the argument to the function:

let img = UIImage(named: "someImage.jpg")!
let analyzedImageColors = function(analyzerClass, selector, img) // <SKUIAnalyzedImageColors: 0x7f90d3408eb0>

I know that analyzedImageColors is of type SKUIAnalyzedImageColors, but the compiler still thinks its type is AnyObject based on the way I declared Prototype above. Now I want to access the properties of an SKUIAnalyzedImageColors object.

From the header, I can see that there are properties such as backgroundColor, textPrimaryColor, and textSecondaryColor on the object. I can access these properties using valueForKey, but I'd like to expose a public interface on SKUIAnalyzedImageColors so I can access these properties.

My first attempt was something like this:

// Create a "forward declaration" of the class
class SKUIAnalyzedImageColors: NSObject { }


// Create convenience extensions for accessing properties
extension SKUIAnalyzedImageColors {
    func backgroundColor() -> UIColor {
        return self.valueForKey("_backgroundColor") as! UIColor
    }

    func textPrimaryColor() -> UIColor {
        return self.valueForKey("_textPrimaryColor") as! UIColor
    }

    func textSecondaryColor() -> UIColor {
        return self.valueForKey("_textSecondaryColor") as! UIColor
    }
}

// ...

// modify the prototype to return an SKUIAnalyzedImageColors object
typealias Prototype = @convention(c) (AnyClass, Selector, UIImage) -> SKUIAnalyzedImageColors?

// ...

// access the properties from the class extension
analyzedImageColors?.backgroundColor() // Optional(UIDeviceRGBColorSpace 0.262745 0.231373 0.337255 1)

This still requires me to use valueForKey. Is there a way to expose a public interface on a class from a framework loaded at runtime?

JAL
  • 41,701
  • 23
  • 172
  • 300

1 Answers1

2

The easiest way to do dynamic Objective-C stuff is to use Objective-C.

ImageAnalyzer.h:

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface SKUIAnalyzedImageColors : NSObject

@property (nonatomic, readonly) UIColor* backgroundColor;
@property (nonatomic, readonly) BOOL isBackgroundLight;
@property (nonatomic, readonly) UIColor* textPrimaryColor;
@property (nonatomic, readonly) UIColor* textSecondaryColor;

@end

SKUIAnalyzedImageColors* _Nullable analyzeImage(UIImage* image);

NS_ASSUME_NONNULL_END

ImageAnalyzer.m:

#import "ImageColorAnalyzer.h"
#include <dlfcn.h>

static Class _SKUIImageColorAnalyzerClass;

@interface SKUIImageColorAnalyzer : NSObject
+ (SKUIAnalyzedImageColors*)analyzeImage:(UIImage*)arg1;
@end

SKUIAnalyzedImageColors* analyzeImage(UIImage* image)
{
    if (!_SKUIImageColorAnalyzerClass)
    {
        if (!dlopen("/System/Library/PrivateFrameworks/StoreKitUI.framework/StoreKitUI", RTLD_NOW))
        {
            NSLog(@"No framework.");
            return nil;
        }
        _SKUIImageColorAnalyzerClass = NSClassFromString(@"SKUIImageColorAnalyzer");
        if (!_SKUIImageColorAnalyzerClass)
        {
            NSLog(@"No Class.");
            return nil;
        }
    }

    return [_SKUIImageColorAnalyzerClass analyzeImage:image];
}

You can then use the analyzeImage function and the SKUIAnalyzedImageColors class easily from either Swift or Objective-C code.

if let image = UIImage(named:"MyImage") {
    if let colors = analyzeImage(image) {
        print("Background Color: \(colors.backgroundColor)")
    }
}

If you really want to do it all in Swift, first declare the parts of the SKUIAnalyzedImageColors Objective-C interface you want to use:

@objc protocol ImageColors {
    var backgroundColor: UIColor { get }
    var isBackgroundLight: Bool { get }
    var textPrimaryColor: UIColor { get }
    var textSecondaryColor: UIColor { get }
}

Then use unsafeBitCast to cast the opaque object instance to your desired Objective-C interface:

let img = UIImage(named: "someImage.jpg")!
let rawAnalyzedImageColors = function(analyzerClass, selector, img) 

let analyzedImageColors = unsafeBitCast(rawAnalyzedImageColors, ImageColors.self)
print("Background color: \(analyzedImageColors.backgroundColor)")
Darren
  • 25,520
  • 5
  • 61
  • 71
  • 1
    Thanks, but I'm really looking for a pure Swift solution without the use of Objective-C. If I wanted to use Objective-C, I could take the headers dumped from the framework and import them through a bridging header to expose the classes and interfaces. – JAL Feb 11 '16 at 22:21
  • Additionally, your pure Swift solution still requires me to expose an interface through a protocol. Is there a way to dynamically load the interface of that object, rather than have to declare everything by hand? – JAL Feb 11 '16 at 22:23
  • I do appreciate your reflection approach though, +1 regardless. – JAL Feb 11 '16 at 22:27
  • @JAL Whether you use Objective-C or Swift you must declare the interface you want to use yourself and then cast the object to that interface. The compiler and the runtime take care of the rest. If it were a public framework then it would provide a header that declares the public interface for you. Because it's private, you have to declare the interface yourself. – Darren Feb 11 '16 at 22:33
  • I understand. I guess i just hoped that loading the classes from the framework would also load each object's interface. It appears that the reflection approach is the best implementation for Swift. Thanks. – JAL Feb 11 '16 at 22:38
  • @JAL There is no reflection here. We're declaring an public interface and telling the compiler that an object instance conforms to that interface. This allows the compiler to use regular Objective-C method dispatch. – Darren Feb 11 '16 at 23:02
  • Ah you're right, I misread the last part of your answer with the unsafeBitCast. – JAL Feb 11 '16 at 23:03
  • I'm going to accept your answer, thank you for your research. – JAL Mar 10 '16 at 14:30