{"id":19856774,"url":"https://github.com/mhdhejazi/dynamic","last_synced_at":"2025-04-04T15:09:50.946Z","repository":{"id":45705180,"uuid":"256725943","full_name":"mhdhejazi/Dynamic","owner":"mhdhejazi","description":"Call hidden/private API in style! The Swift way. ","archived":false,"fork":false,"pushed_at":"2021-10-02T15:28:08.000Z","size":83,"stargazers_count":718,"open_issues_count":17,"forks_count":39,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-03-28T14:08:06.349Z","etag":null,"topics":["bridging","dynamic-subscripts","objective-c","private-api","swift"],"latest_commit_sha":null,"homepage":"https://medium.com/@mhdhejazi/calling-ios-and-macos-hidden-api-in-style-1a924f244ad1","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mhdhejazi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-04-18T10:32:15.000Z","updated_at":"2025-03-27T01:03:49.000Z","dependencies_parsed_at":"2022-09-11T04:40:36.569Z","dependency_job_id":null,"html_url":"https://github.com/mhdhejazi/Dynamic","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhdhejazi%2FDynamic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhdhejazi%2FDynamic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhdhejazi%2FDynamic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhdhejazi%2FDynamic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mhdhejazi","download_url":"https://codeload.github.com/mhdhejazi/Dynamic/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247198463,"owners_count":20900080,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bridging","dynamic-subscripts","objective-c","private-api","swift"],"created_at":"2024-11-12T14:16:32.806Z","updated_at":"2025-04-04T15:09:50.909Z","avatar_url":"https://github.com/mhdhejazi.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"![image](https://user-images.githubusercontent.com/121827/79637117-4961c880-8185-11ea-9014-5eb7fc9dc211.png)\n\n![Swift](https://img.shields.io/badge/Swift-5.2-orange?logo=Swift\u0026logoColor=white)\n[![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg)](https://swift.org/package-manager/)\n![Build](https://github.com/mhdhejazi/Dynamic/workflows/Build/badge.svg)\n![Tests](https://github.com/mhdhejazi/Dynamic/workflows/Tests/badge.svg)\n\nA library that uses `@dynamicMemberLookup` and `@dynamicCallable` to access Objective-C API the Swifty way.\n\n## Table of contents\n\n  * [Introduction](#introduction)\n  * [Examples](#examples)\n  * [Installation](#installation)\n  * [How to use](#how-to-use)\n  * [Requirements](#requirements)\n  * [Contribution](#contribution)\n  * [Author](#author)\n\n## Introduction\n\nAssume we have the following private Objective-C class that we want to access in Swift:\n```objectivec\n@interface Toolbar : NSObject\n- (NSString *)titleForItem:(NSString *)item withTag:(NSString *)tag;\n@end\n```\nThere are three ways to dynamically call the method in this class:\n\n**1. Using `performSelector()`**\n```swift\nlet selector = NSSelectorFromString(\"titleForItem:withTag:\")\nlet unmanaged = toolbar.perform(selector, with: \"foo\", with: \"bar\")\nlet result = unmanaged?.takeRetainedValue() as? String\n```\n\n**2. Using `methodForSelector()` with `@convention(c)`**\n```swift\ntypealias titleForItemMethod = @convention(c)\n    (NSObject, Selector, NSString, NSString) -\u003e NSString\n  \nlet selector = NSSelectorFromString(\"titleForItem:withTag:\")\nlet methodIMP = toolbar.method(for: selector)\nlet method = unsafeBitCast(methodIMP, to: titleForItemMethod.self)\nlet result = method(toolbar, selector, \"foo\", \"bar\")\n```\n\n**3. Using `NSInvocation`**\n\u003cdetails\u003e\n\u003csummary\u003eIt's only available in Objective-C.\u003c/summary\u003e\n\n```objectivec\nSEL selector = @selector(titleForItem:withTag:);\nNSMethodSignature *signature = [toolbar methodSignatureForSelector:selector];\n\nNSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];\ninvocation.target = toolbar;\ninvocation.selector = selector;\n\nNSString *argument1 = @\"foo\";\nNSString *argument2 = @\"bar\";\n[invocation setArgument:\u0026argument1 atIndex:2];\n[invocation setArgument:\u0026argument2 atIndex:3];\n\n[invocation invoke];\n\nNSString *result;\n[invocation getReturnValue:\u0026result];\n```\n\u003c/details\u003e\n\n**Or, we can use Dynamic** 🎉 \n\n```swift\nlet result = Dynamic(toolbar)            // Wrap the object with Dynamic\n    .titleForItem(\"foo\", withTag: \"bar\") // Call the method directly!\n```\n\n\u003e More details on how the library is designed and how it works [here](https://medium.com/swlh/calling-ios-and-macos-hidden-api-in-style-1a924f244ad1).\n\n## Examples\nThe main use cases for  `Dynamic`  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.\n\nWhat follows are examples of how easy it is to access AppKit API in a Mac Catalyst with the help of Dynamic.\n\n#### 1. Get the `NSWindow` from a `UIWindow` in a MacCatalyst app\n```swift\nextension UIWindow {\n    var nsWindow: NSObject? {\n        var nsWindow = Dynamic.NSApplication.sharedApplication.delegate.hostWindowForUIWindow(self)\n        if #available(macOS 11, *) {\n            nsWindow = nsWindow.attachedWindow\n        }\n        return nsWindow.asObject\n    }\n}\n```\n\n#### 2. Enter fullscreen in a MacCatalyst app\n```swift\n// macOS App\nwindow.toggleFullScreen(nil)\n\n// Mac Catalyst (with Dynamic)\nwindow.nsWindow.toggleFullScreen(nil)\n```\n\n#### 3. Using `NSOpenPanel` in a MacCatalyst app\n```swift\n// macOS App\nlet panel = NSOpenPanel()\npanel.beginSheetModal(for: view.window!, completionHandler: { response in\n    if let url: URL = panel.urls.first {\n        print(\"url: \", url)\n    }\n})\n\n// Mac Catalyst (with Dynamic)\nlet panel = Dynamic.NSOpenPanel()\npanel.beginSheetModalForWindow(self.view.window!.nsWindow, completionHandler: { response in\n    if let url: URL = panel.URLs.firstObject {\n        print(\"url: \", url)\n    }\n} as ResponseBlock)\n\ntypealias ResponseBlock = @convention(block) (_ response: Int) -\u003e Void\n```\n\n#### 4. Change the window scale factor in MacCatalyst apps\niOS views in Mac Catalyst apps are automatically scaled down to 77%. To change the scale factor we need to access a hidden property:\n```swift\noverride func viewDidAppear(_ animated: Bool) {\n  view.window?.scaleFactor = 1.0 // Default value is 0.77\n}\n\nextension UIWindow {\n  var scaleFactor: CGFloat {\n    get {\n      Dynamic(view.window?.nsWindow).contentView\n        .subviews.firstObject.scaleFactor ?? 1.0\n    }\n    set {\n      Dynamic(view.window?.nsWindow).contentView\n        .subviews.firstObject.scaleFactor = newValue\n    }\n  }\n}\n```\n\n## Installation\nYou can use [Swift Package Manager](https://swift.org/package-manager)  to install  `Dynamic`  by adding it in your  `Package.swift` :\n\n```swift\nlet package = Package(\n    dependencies: [\n        .package(url: \"https://github.com/mhdhejazi/Dynamic.git\", branch: \"master\")\n    ]\n)\n```\n\n## How to use\nThe following diagram shows how we use Dynamic to access private properties and methods from the Objective-C object `obj`:\n![Diagram](https://user-images.githubusercontent.com/121827/83970645-7312b280-a8df-11ea-87bf-d69682f8627d.png)\n\n\n### 1. Wrap Objective-C objects\nTo work with Objective-C classes and instances, we need to wrap them with Dynamic first\n\n#### Wrapping an existing object\nIf we have a reference for an existing Objective-C object, we can simply wrap it with `Dynamic`:\n```swift\nlet dynamicObject = Dynamic(objcObject)\n```\n\n#### Creating new instances\nTo create a new instance from a hidden class, we prepend its name with `Dynamic` (or `ObjC`):\n```swift\n// Objective-C:\n[[NSDateFormatter alloc] init];\n\n// Swift:\nlet formatter = Dynamic.NSDateFormatter()\n// Or maybe:\nlet formatter = ObjC.NSDateFormatter()\n// Or the longer form:\nlet formatter = ObjC.NSDateFormatter.`init`()\n```\n\u003e **Note 1**: The `formatter` is an instance of `Dynamic` that wraps the new instance of `NSDateFormatter`\n\n\u003e **Note 2**: `ObjC` is just a typealias for `Dynamic`. Whatever you choose to use, stay consistent.\n\nIf the initializer takes parameters, we can pass them directly:\n```swift\n// Objective-C:\n[[NSProgress alloc] initWithParent:foo userInfo:bar];\n\n// Swift:\nlet progress = Dynamic.NSProgress(parent: foo, userInfo: bar)\n// Or the longer form:\nlet progress = Dynamic.NSProgress.initWithParent(foo, userInfo: bar)\n```\n\u003e Both forms are equivalent because the library adds the prefix `initWith` to the method selector in the first case.\n\u003e If you choose to use the shorter form, remember that you can only drop the prefix `initWith` from the original initializer name. Whatever comes after `initWith` should be the label of the first parameter.\n\n#### Singletons\nAccessing singletons is also straightforward:\n```swift\n// Objective-C:\n[NSApplication sharedApplication];\n\n// Swift:\nlet app = Dynamic.NSApplication.sharedApplication()\n// Or we can drop the parenthesizes, as if `sharedApplication` was a static property:\nlet app = Dynamic.NSApplication.sharedApplication\n```\n\n\u003e **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 is [`shared`](https://developer.apple.com/documentation/appkit/nsapplication/1428360-shared) not `sharedApplication`, but we can only use [`sharedApplicaton`](https://developer.apple.com/documentation/appkit/nsapplication/1428360-sharedapplication) here as we're internally taking with the Objective-C classes.\n\u003e Always refer to the Objective-C documentation of the method you're trying to call to make sure you're using the right name.\n\n### 2. Call the private API\nAfter wrapping the Objective-C object, we can now access its properties and methods directly from the Dynamic object.\n\n#### Accessing properties\n```swift\n// Objective-C:\n@interface NSDateFormatter {\n  @property(copy) NSString *dateFormat;\n}\n\n// Swift:\nlet formatter = Dynamic.NSDateFormatter()\n// Getting the property value:\nlet format = formatter.dateFormat // `format` is now a Dynamic object\n// Setting the property value:\nformatter.dateFormat = \"yyyy-MM-dd\"\n// Or the longer version:\nformatter.dateFormat = NSString(\"yyyy-MM-dd\")\n```\n\u003e **Note 1**: The variable `format` above is now a `Dynamic` object that wraps the actual property value. The reason for returning a `Dynamic` object and not the actual value is to allow call chaining. We'll see later how we can unwrap the actual value from a `Dynamic` object.\n\n\u003e **Note 2**: Although the property `NSDateFormatter.dataFormat` is of the type `NSString`, we can set it to a Swift `String` and the library will convert it to `NSString` automatically.\n\n#### Calling methods\n```swift\nlet formatter = Dynamic.NSDateFormatter()\nlet date = formatter.dateFromString(\"2020 Mar 30\") // `date` is now a Dynamic object\n```\n```swift\n// Objective-C:\n[view resizeSubviewsWithOldSize:size];\n[view beginPageInRect:rect atPlacement:point];\n\n// Swift:\nview.resizeSubviewsWithOldSize(size) // OR ⤸\nview.resizeSubviews(withOldSize: size)\n\nview.beginPageInRect(rect, atPlacement: point) // OR ⤸\nview.beginPage(inRect: rect, atPlacement: point)\n```\n\u003e 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.\n\n#### Objective-C block arguments\nTo 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.\n```swift\n// Objective-C:\n- (void)beginSheetModalForWindow:(NSWindow *)sheetWindow \n               completionHandler:(void (^)(NSModalResponse returnCode))handler;\n\n// Swift:\nlet panel = Dynamic.NSOpenPanel.openPanel()\npanel.beginSheetModal(forWindow: window, completionHandler: { result in\n    print(\"result: \", result)\n} as ResultBlock)\n\ntypealias ResultBlock = @convention(block) (_ result: Int) -\u003e Void\n```\n\n### 3. Unwrap the result\nMethods and properties return `Dynamic` objects by default to make it possible to chain calls. When the actual value is needed it can be unwrapped in multiple ways:\n\n#### Implicit unwrapping\nA value can be implicitly unwrapped by simply specifying the type of the variable we're assigning the result to. \n```swift\nlet formatter = Dynamic.NSDateFormatter()\nlet date: Date? = formatter.dateFromString(\"2020 Mar 30\") // Implicitly unwrapped as Date?\nlet format: String? = formatter.dateFormat // Implicitly unwrapped as String?\n\nlet progress = Dynamic.NSProgress()\nlet total: Int? = progress.totalUnitCount // Implicitly unwrapped as Int?\n```\nNote that we should always use a nullable type (`Optional`) for the variable type or we may see a compiler error:\n```swift\nlet total = progress.totalUnitCount // No unwrapping. `total` is a Dynamic object\nlet total: Int? = progress.totalUnitCount // Implicit unwrapping as Int?\nlet total: Int = progress.totalUnitCount // Compiler error\nlet total: Int = progress.totalUnitCount! // Okay, but dangerous\n```\n\n\u003e 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.\n\nNote 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:\n```swift\n// This is okay:\nlet format: Date? = formatter.dateFromString(\"2020 Mar 30\")\n\n// But this is not:\nlet dynamicObj = formatter.dateFromString(\"2020 Mar 30\")\nlet format: Date? = dynamicObj // Compiler error\n```\n\n#### Explicit unwrapping\nWe can also explicitly unwrap values by calling one of the `as\u003cType\u003e` properties:\n```swift\nDynamic.NSDateFormatter().asObject // Returns the wrapped value as NSObject?\nformatter.dateFormat.asString // Returns the wrapped value as String?\nprogress.totalUnitCount.asInt // Returns the wrapped value as Int?\n```\nAnd there are many properties for different kinds of values:\n```swift\nvar asAnyObject: AnyObject? { get }\nvar asValue: NSValue? { get }\nvar asObject: NSObject? { get }\nvar asArray: NSArray? { get }\nvar asDictionary: NSDictionary? { get }\nvar asString: String? { get }\nvar asFloat: Float? { get }\nvar asDouble: Double? { get }\nvar asBool: Bool? { get }\nvar asInt: Int? { get }\nvar asSelector: Selector? { get }\nvar asCGPoint: CGPoint? { get }\nvar asCGVector: CGVector? { get }\nvar asCGSize: CGSize? { get }\nvar asCGRect: CGRect? { get }\nvar asCGAffineTransform: CGAffineTransform? { get }\nvar asUIEdgeInsets: UIEdgeInsets? { get }\nvar asUIOffset: UIOffset? { get }\nvar asCATransform3D: CATransform3D? { get }\n```\n### Edge cases\n#### Unrecognized methods and properties\nIf you try to access undefined properties or methods the app won't crash, but you'll get `InvocationError.unrecognizedSelector` wrapped with a `Dynamic` object. You can use `Dynamic.isError` to check for such an error.\n```swift\nlet result = Dynamic.NSDateFormatter().undefinedMethod()\nresult.isError // -\u003e true\n```\nAnd you'll also see a warning in the console:\n```nasm\nWARNING: Trying to access an unrecognized member: NSDateFormatter.undefinedMethod\n```\n\u003e Note that a crash may expectedly happen if you pass random parameters of unexpected types to a method that doesn't expect them.\n\n#### Setting a property to `nil`\nYou can use one the following ways to set a property to `nil`:\n```swift\nformatter.dateFormat = .nil           // The custom Dynamic.nil constant\nformatter.dateFormat = nil as String? // A \"typed\" nil\nformatter.dateFormat = String?.none   // The Optional.none case\n```\n\n### Logging\nIt's always good to understand what's happening under the hood - be it to debug a problem or just out of curiosity.\nTo enable extensive logging, simply change the `loggingEnabled` property to `true`:\n```swift\nDynamic.loggingEnabled = true\n```\n\n## Requirements\n\n#### Swift: 5.0\n`Dynamic` uses the `@dynamicCallable` attribute which was introduced in Swift 5.\n\n## Contribution\nPlease feel free to contribute pull requests, or create issues for bugs and feature requests.\n\n## Author\nMhd Hejazi \u003ca href=\"https://twitter.com/intent/follow?screen_name=Hejazi\"\u003e\u003cimg src=\"https://img.shields.io/badge/@hejazi-x?color=08a0e9\u0026logo=twitter\u0026logoColor=white\" valign=\"middle\" /\u003e\u003c/a\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmhdhejazi%2Fdynamic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmhdhejazi%2Fdynamic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmhdhejazi%2Fdynamic/lists"}