Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Call hidden/private API in style! The Swift way.

License

NotificationsYou must be signed in to change notification settings

mhdhejazi/Dynamic

Repository files navigation

image

SwiftSwiftPM compatibleBuildTests

A library that uses@dynamicMemberLookup and@dynamicCallable to access Objective-C API the Swifty way.

Table of contents

Introduction

Assume we have the following private Objective-C class that we want to access in Swift:

@interfaceToolbar :NSObject- (NSString *)titleForItem:(NSString *)itemwithTag:(NSString *)tag;@end

There are three ways to dynamically call the method in this class:

1. UsingperformSelector()

letselector=NSSelectorFromString("titleForItem:withTag:")letunmanaged= toolbar.perform(selector, with:"foo", with:"bar")letresult= unmanaged?.takeRetainedValue()as?String

2. UsingmethodForSelector() with@convention(c)

typealiastitleForItemMethod=@convention(c)(NSObject,Selector,NSString,NSString)->NSStringletselector=NSSelectorFromString("titleForItem:withTag:")letmethodIMP= toolbar.method(for: selector)letmethod=unsafeBitCast(methodIMP, to: titleForItemMethod.self)letresult=method(toolbar, selector,"foo","bar")

3. UsingNSInvocation

It's only available in Objective-C.
SEL selector =@selector(titleForItem:withTag:);NSMethodSignature *signature = [toolbarmethodSignatureForSelector:selector];NSInvocation *invocation = [NSInvocationinvocationWithMethodSignature:signature];invocation.target = toolbar;invocation.selector = selector;NSString *argument1 =@"foo";NSString *argument2 =@"bar";[invocationsetArgument:&argument1atIndex:2];[invocationsetArgument:&argument2atIndex:3];[invocationinvoke];NSString *result;[invocationgetReturnValue:&result];

Or, we can use Dynamic 🎉

letresult=Dynamic(toolbar)            // Wrap the object with Dynamic.titleForItem("foo", withTag:"bar") // Call the method directly!

More details on how the library is designed and how it workshere.

Examples

The main use cases forDynamic is accessing private/hidden iOS and macOS API in Swift. And with the introduction of Mac Catalyst, the need to access hidden API arose as Apple only made a very small portion of the macOS AppKit API visible to Catalyst apps.

What follows are examples of how easy it is to access AppKit API in a Mac Catalyst with the help of Dynamic.

1. Get theNSWindow from aUIWindow in a MacCatalyst app

extensionUIWindow{varnsWindow:NSObject?{varnsWindow=Dynamic.NSApplication.sharedApplication.delegate.hostWindowForUIWindow(self)if #available(macOS11,*){            nsWindow= nsWindow.attachedWindow}return nsWindow.asObject}}

2. Enter fullscreen in a MacCatalyst app

// macOS Appwindow.toggleFullScreen(nil)// Mac Catalyst (with Dynamic)window.nsWindow.toggleFullScreen(nil)

3. UsingNSOpenPanel in a MacCatalyst app

// macOS Appletpanel=NSOpenPanel()panel.beginSheetModal(for: view.window!, completionHandler:{ responseiniflet url:URL= panel.urls.first{print("url:", url)}})// Mac Catalyst (with Dynamic)letpanel=Dynamic.NSOpenPanel()panel.beginSheetModalForWindow(self.view.window!.nsWindow, completionHandler:{ responseiniflet url:URL= panel.URLs.firstObject{print("url:", url)}}asResponseBlock)typealiasResponseBlock=@convention(block)(_ response:Int)->Void

4. Change the window scale factor in MacCatalyst apps

iOS views in Mac Catalyst apps are automatically scaled down to 77%. To change the scale factor we need to access a hidden property:

overridefunc viewDidAppear(_ animated:Bool){  view.window?.scaleFactor=1.0 // Default value is 0.77}extensionUIWindow{varscaleFactor:CGFloat{get{Dynamic(view.window?.nsWindow).contentView.subviews.firstObject.scaleFactor??1.0}set{Dynamic(view.window?.nsWindow).contentView.subviews.firstObject.scaleFactor= newValue}}}

Installation

You can useSwift Package Manager to installDynamic by adding it in yourPackage.swift :

letpackage=Package(    dependencies:[.package(url:"https://github.com/mhdhejazi/Dynamic.git", branch:"master")])

How to use

The following diagram shows how we use Dynamic to access private properties and methods from the Objective-C objectobj:Diagram

1. Wrap Objective-C objects

To work with Objective-C classes and instances, we need to wrap them with Dynamic first

Wrapping an existing object

If we have a reference for an existing Objective-C object, we can simply wrap it withDynamic:

letdynamicObject=Dynamic(objcObject)

Creating new instances

To create a new instance from a hidden class, we prepend its name withDynamic (orObjC):

// Objective-C:[[NSDateFormatter alloc] init];// Swift:letformatter=Dynamic.NSDateFormatter()// Or maybe:letformatter=ObjC.NSDateFormatter()// Or the longer form:letformatter=ObjC.NSDateFormatter.`init`()

Note 1: Theformatter is an instance ofDynamic that wraps the new instance ofNSDateFormatter

Note 2:ObjC is just a typealias forDynamic. Whatever you choose to use, stay consistent.

If the initializer takes parameters, we can pass them directly:

// Objective-C:[[NSProgress alloc] initWithParent:foo userInfo:bar];// Swift:letprogress=Dynamic.NSProgress(parent: foo, userInfo: bar)// Or the longer form:letprogress=Dynamic.NSProgress.initWithParent(foo, userInfo: bar)

Both forms are equivalent because the library adds the prefixinitWith to the method selector in the first case.If you choose to use the shorter form, remember that you can only drop the prefixinitWith from the original initializer name. Whatever comes afterinitWith should be the label of the first parameter.

Singletons

Accessing singletons is also straightforward:

// Objective-C:[NSApplication sharedApplication];// Swift:letapp=Dynamic.NSApplication.sharedApplication()// Or we can drop the parenthesizes, as if `sharedApplication` was a static property:letapp=Dynamic.NSApplication.sharedApplication

Important Note: Although the syntax looks very similar to the Swift API, it's not always identical to the Swift version of the used API. For instance, the name of the above singleton in Swift isshared notsharedApplication, but we can only usesharedApplicaton here as we're internally taking with the Objective-C classes.Always refer to the Objective-C documentation of the method you're trying to call to make sure you're using the right name.

2. Call the private API

After wrapping the Objective-C object, we can now access its properties and methods directly from the Dynamic object.

Accessing properties

// Objective-C:@interface NSDateFormatter{@property(copy) NSString*dateFormat;}// Swift:letformatter=Dynamic.NSDateFormatter()// Getting the property value:letformat= formatter.dateFormat // `format` is now a Dynamic object// Setting the property value:formatter.dateFormat="yyyy-MM-dd"// Or the longer version:formatter.dateFormat=NSString("yyyy-MM-dd")

Note 1: The variableformat above is now aDynamic object that wraps the actual property value. The reason for returning aDynamic object and not the actual value is to allow call chaining. We'll see later how we can unwrap the actual value from aDynamic object.

Note 2: Although the propertyNSDateFormatter.dataFormat is of the typeNSString, we can set it to a SwiftString and the library will convert it toNSString automatically.

Calling methods

letformatter=Dynamic.NSDateFormatter()letdate= formatter.dateFromString("2020 Mar 30") // `date` is now a Dynamic object
// Objective-C:[view resizeSubviewsWithOldSize:size];[view beginPageInRect:rect atPlacement:point];// Swift:view.resizeSubviewsWithOldSize(size) // OR ⤸view.resizeSubviews(withOldSize: size)view.beginPageInRect(rect, atPlacement: point) // OR ⤸view.beginPage(inRect: rect, atPlacement: point)

Calling the same method in different forms is possible because the library combines the method name (e.g.resizeSubviews) with the first parameter label (e.g.withOldSize) to form the method selector (e.g.resizeSubviewsWithOldSize:). This means you can also call:view.re(sizeSubviewsWithOldSize: size), but please don't.

Objective-C block arguments

To pass a Swift closure for a block argument, we need to add@convention(block) to the closure type, and then cast the passed closure to this type.

// Objective-C:-(void)beginSheetModalForWindow:(NSWindow*)sheetWindowcompletionHandler:(void(^)(NSModalResponsereturnCode))handler;// Swift:letpanel=Dynamic.NSOpenPanel.openPanel()panel.beginSheetModal(forWindow: window, completionHandler:{ resultinprint("result:", result)}asResultBlock)typealiasResultBlock=@convention(block)(_ result:Int)->Void

3. Unwrap the result

Methods and properties returnDynamic objects by default to make it possible to chain calls. When the actual value is needed it can be unwrapped in multiple ways:

Implicit unwrapping

A value can be implicitly unwrapped by simply specifying the type of the variable we're assigning the result to.

letformatter=Dynamic.NSDateFormatter()letdate:Date?= formatter.dateFromString("2020 Mar 30") // Implicitly unwrapped as Date?letformat:String?= formatter.dateFormat // Implicitly unwrapped as String?letprogress=Dynamic.NSProgress()lettotal:Int?= progress.totalUnitCount // Implicitly unwrapped as Int?

Note that we should always use a nullable type (Optional) for the variable type or we may see a compiler error:

lettotal= progress.totalUnitCount // No unwrapping. `total` is a Dynamic objectlettotal:Int?= progress.totalUnitCount // Implicit unwrapping as Int?lettotal:Int= progress.totalUnitCount // Compiler errorlettotal:Int= progress.totalUnitCount! // Okay, but dangerous

Assigning to a variable of an optional type isn't the only way for implicitly unwrapping a value. Other ways include returning the result of a method call or comparing it with a variable of an optional type.

Note that the implicit unwrapping only works with properties and method calls since the compiler can choose the proper overloading method based on the expected type. This isn't the case when we simply return a Dynamic variable or assign it to another variable:

// This is okay:letformat:Date?= formatter.dateFromString("2020 Mar 30")// But this is not:letdynamicObj= formatter.dateFromString("2020 Mar 30")letformat:Date?= dynamicObj // Compiler error

Explicit unwrapping

We can also explicitly unwrap values by calling one of theas<Type> properties:

Dynamic.NSDateFormatter().asObject // Returns the wrapped value as NSObject?formatter.dateFormat.asString // Returns the wrapped value as String?progress.totalUnitCount.asInt // Returns the wrapped value as Int?

And there are many properties for different kinds of values:

varasAnyObject:AnyObject?{get}varasValue:NSValue?{get}varasObject:NSObject?{get}varasArray:NSArray?{get}varasDictionary:NSDictionary?{get}varasString:String?{get}varasFloat:Float?{get}varasDouble:Double?{get}varasBool:Bool?{get}varasInt:Int?{get}varasSelector:Selector?{get}varasCGPoint:CGPoint?{get}varasCGVector:CGVector?{get}varasCGSize:CGSize?{get}varasCGRect:CGRect?{get}varasCGAffineTransform:CGAffineTransform?{get}varasUIEdgeInsets:UIEdgeInsets?{get}varasUIOffset:UIOffset?{get}varasCATransform3D:CATransform3D?{get}

Edge cases

Unrecognized methods and properties

If you try to access undefined properties or methods the app won't crash, but you'll getInvocationError.unrecognizedSelector wrapped with aDynamic object. You can useDynamic.isError to check for such an error.

letresult=Dynamic.NSDateFormatter().undefinedMethod()result.isError // -> true

And you'll also see a warning in the console:

WARNING: Trying to access an unrecognized member: NSDateFormatter.undefinedMethod

Note that a crash may expectedly happen if you pass random parameters of unexpected types to a method that doesn't expect them.

Setting a property tonil

You can use one the following ways to set a property tonil:

formatter.dateFormat=.nil           // The custom Dynamic.nil constantformatter.dateFormat=nilasString? // A "typed" nilformatter.dateFormat=String?.none   // The Optional.none case

Logging

It's always good to understand what's happening under the hood - be it to debug a problem or just out of curiosity.To enable extensive logging, simply change theloggingEnabled property totrue:

Dynamic.loggingEnabled=true

Requirements

Swift: 5.0

Dynamic uses the@dynamicCallable attribute which was introduced in Swift 5.

Contribution

Please feel free to contribute pull requests, or create issues for bugs and feature requests.

Author

Mhd Hejazi


[8]ページ先頭

©2009-2025 Movatter.jp