How to monitor a folder for new files in swift? - macos

How would I monitor a folder for new files in swift, without polling (which is very inefficient)? I've heard of APIs such as kqueue and FSEvents - but I'm not sure it's possible to implement them in swift?

GCD seems to be the way to go. NSFilePresenter classes doesn't work properly. They're buggy, broken, and Apple is haven't willing to fix them for last 4 years. Likely to be deprecated.
Here's a very nice posting which describes essentials of this technique.
"Handling Filesystem Events with GCD", by David Hamrick.
Sample code cited from the website. I translated his C code into Swift.
let fildes = open("/path/to/config.plist", O_RDONLY)
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let source = dispatch_source_create(
DISPATCH_SOURCE_TYPE_VNODE,
UInt(fildes),
DISPATCH_VNODE_DELETE | DISPATCH_VNODE_WRITE | DISPATCH_VNODE_EXTEND | DISPATCH_VNODE_ATTRIB | DISPATCH_VNODE_LINK | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_REVOKE,
queue)
dispatch_source_set_event_handler(source,
{
//Reload the config file
})
dispatch_source_set_cancel_handler(source,
{
//Handle the cancel
})
dispatch_resume(source);
...
// sometime later
dispatch_source_cancel(source);
For reference, here're another QAs posted by the author:
Grand Central Dispatch (GCD) dispatch source flags
Monitoring a directory in Cocoa/Cocoa Touch
If you're interested in watching directories, here's another posting which describes it.
"Monitoring a Folder with GCD" on Cocoanetics. (unfortunately, I couldn't find the author's name. I am sorry for lacking attribution)
The only noticeable difference is getting a file-descriptor. This makes event-notification-only file descriptor for a directory.
_fileDescriptor = open(path.fileSystemRepresentation(), O_EVTONLY)
Update
Previously I claimed FSEvents API is not working, but I was wrong. The API is working very well, and if you're interested in watching on deep file tree, than it can be better then GCD by its simplicity.
Anyway, FSEvents cannot be used in pure Swift programs. Because it requires passing of C callback function, and Swift does not support it currently (Xcode 6.1.1). Then I had to fallback to Objective-C and wrap it again.
Also, any of this kind API is all fully asynchronous. That means actual file system state can be different at the time you are receiving the notifications. Then precise or accurate notification is not really helpful, and useful only for marking a dirty flag.
Update 2
I finally ended up with writing a wrapper around FSEvents for Swift.
Here's my work, and I hope this to be helpful.
https://github.com/eonil/FileSystemEvents

I adapted Stanislav Smida's code to make it work with Xcode 8 and Swift 3
class DirectoryObserver {
private let fileDescriptor: CInt
private let source: DispatchSourceProtocol
deinit {
self.source.cancel()
close(fileDescriptor)
}
init(URL: URL, block: #escaping ()->Void) {
self.fileDescriptor = open(URL.path, O_EVTONLY)
self.source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: self.fileDescriptor, eventMask: .all, queue: DispatchQueue.global())
self.source.setEventHandler {
block()
}
self.source.resume()
}
}

The simplest solution is to use Apple's DirectoryMonitor.swift
https://github.com/Lax/Learn-iOS-Swift-by-Examples/blob/master/Lister/ListerKit/DirectoryMonitor.swift
var dm = DirectoryMonitor(URL: AppDelegate.applicationDocumentsDirectory)
dm.delegate = self
dm.startMonitoring()

Swift 5 Version for Directory Monitor, with GCD, original from Apple
import Foundation
/// A protocol that allows delegates of `DirectoryMonitor` to respond to changes in a directory.
protocol DirectoryMonitorDelegate: class {
func directoryMonitorDidObserveChange(directoryMonitor: DirectoryMonitor)
}
class DirectoryMonitor {
// MARK: Properties
/// The `DirectoryMonitor`'s delegate who is responsible for responding to `DirectoryMonitor` updates.
weak var delegate: DirectoryMonitorDelegate?
/// A file descriptor for the monitored directory.
var monitoredDirectoryFileDescriptor: CInt = -1
/// A dispatch queue used for sending file changes in the directory.
let directoryMonitorQueue = DispatchQueue(label: "directorymonitor", attributes: .concurrent)
/// A dispatch source to monitor a file descriptor created from the directory.
var directoryMonitorSource: DispatchSource?
/// URL for the directory being monitored.
var url: URL
// MARK: Initializers
init(url: URL) {
self.url = url
}
// MARK: Monitoring
func startMonitoring() {
// Listen for changes to the directory (if we are not already).
if directoryMonitorSource == nil && monitoredDirectoryFileDescriptor == -1 {
// Open the directory referenced by URL for monitoring only.
monitoredDirectoryFileDescriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY)
// Define a dispatch source monitoring the directory for additions, deletions, and renamings.
directoryMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredDirectoryFileDescriptor, eventMask: DispatchSource.FileSystemEvent.write, queue: directoryMonitorQueue) as? DispatchSource
// Define the block to call when a file change is detected.
directoryMonitorSource?.setEventHandler{
// Call out to the `DirectoryMonitorDelegate` so that it can react appropriately to the change.
self.delegate?.directoryMonitorDidObserveChange(directoryMonitor: self)
}
// Define a cancel handler to ensure the directory is closed when the source is cancelled.
directoryMonitorSource?.setCancelHandler{
close(self.monitoredDirectoryFileDescriptor)
self.monitoredDirectoryFileDescriptor = -1
self.directoryMonitorSource = nil
}
// Start monitoring the directory via the source.
directoryMonitorSource?.resume()
}
}
func stopMonitoring() {
// Stop listening for changes to the directory, if the source has been created.
if directoryMonitorSource != nil {
// Stop monitoring the directory via the source.
directoryMonitorSource?.cancel()
}
}
}

I've tried to go with these few lines. So far seems to work.
class DirectoryObserver {
deinit {
dispatch_source_cancel(source)
close(fileDescriptor)
}
init(URL: NSURL, block: dispatch_block_t) {
fileDescriptor = open(URL.path!, O_EVTONLY)
source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, UInt(fileDescriptor), DISPATCH_VNODE_WRITE, dispatch_queue_create(nil, DISPATCH_QUEUE_CONCURRENT))
dispatch_source_set_event_handler(source, { dispatch_async(dispatch_get_main_queue(), block) })
dispatch_resume(source)
}
//
private let fileDescriptor: CInt
private let source: dispatch_source_t
}
Be sure to not get into retain cycle. If you are going to use owner of this instance in block, do it safely. For example:
self.directoryObserver = DirectoryObserver(URL: URL, block: { [weak self] in
self?.doSomething()
})

SKQueue is a Swift wrapper around kqueue. Here is sample code that watches a directory and notifies of write events.
class SomeClass: SKQueueDelegate {
func receivedNotification(_ notification: SKQueueNotification, path: String, queue: SKQueue) {
print("\(notification.toStrings().map { $0.rawValue }) # \(path)")
}
}
if let queue = SKQueue() {
let delegate = SomeClass()
queue.delegate = delegate
queue.addPath("/some/file/or/directory")
queue.addPath("/some/other/file/or/directory")
}

Easiest method I've found that I'm currently using is this wonderful library: https://github.com/eonist/FileWatcher
From README
Installation:
CocoaPods pod "FileWatcher"
Carthage github "eonist/FileWatcher" "master"
Manual Open FileWatcherExample.xcodeproj
let filewatcher = FileWatcher([NSString(string: "~/Desktop").expandingTildeInPath])
filewatcher.callback = { event in
print("Something happened here: " + event.path)
}
filewatcher.start() // start monitoring

I faced a problem that is not mentioned in any of the answers. As my app is using a UIDocumentBrowserViewController (i.e. Apple's own Files app) to manage its documents, I have no control over my users' habits. I was using SKQueue to monitor all files in order to keep metadata in sync, and at a certain point the app started crashing.
As it turns out, there is an upper limit of 256 file descriptors that can be open by an app simultaneously, even just for monitoring. I ended up combining SKQueue and Apple's Directory Monitor (reference to which you can find in this answer of the current thread) to create a class I named SFSMonitor, which monitors a whole queue of files or directories by using Dispatch Sources.
I detailed my findings and the practices I now use in this SO thread.

You could add UKKQueue to your project. See http://zathras.de/angelweb/sourcecode.htm it's easy to use. UKKQueue is written in Objective C, but you can use it from swift

Depending on your application needs, you may be able to use a simple solution.
I actually used kqueue in a production product; I wasn't crazy with the performance but it worked, so I didn't think too much of it till I found a nice little trick that worked even better for my needs, plus, it used less resources which can be important for performance intensive programs.
What you can do, again, if your project permits, is that every time you switch to your application, you can just check the folder as part of your logic, instead of having to periodically check the folder using kqueue. This works and uses far less resources.

Related

Get POSIX path of active Finder window with JXA AppleScript

I would like the JXA equivalent of this AppleScript snippet:
tell application "Finder"
# Get path
set currentTarget to target of window 1
set posixPath to (POSIX path of (currentTarget as alias))
# Show dialog
display dialog posixPath buttons {"OK"}
end tell
The closest I got was using the url property to initialize a Foundation NSURL object and access its fileSystemRepresentation property like so:
// Get path
var finder = Application('Finder')
var currentTarget = finder.finderWindows[0].target()
var fileURLString = currentTarget.url()
// I'd like to get rid of this step
var fileURL = $.NSURL.alloc.initWithString(fileURLString)
var posixPath = fileURL.fileSystemRepresentation
// Show dialog
finder.includeStandardAdditions = true
finder.displayAlert('', {buttons: ['Ok'], message: posixPath})
But this seems unnecessarily complex. Is there a nicer way to get to the POSIX path without using Foundation API or manual string wrangling?
If I naively try this:
finder.finderWindows[0].target().posixPath()
I get this error:
app.startupDisk.folders.byName("Users").folders.byName("kymer").folders.byName("Desktop").posixPath()
--> Error -1728: Can't get object.
This SO answer seems relevant, but I can't seem to adapt it to fit my needs:
App = Application.currentApplication()
App.includeStandardAdditions = true
SystemEvents = Application('System Events')
var pathToMe = App.pathTo(this)
var containerPOSIXPath = SystemEvents.files[pathToMe.toString()].container().posixPath()
Any help would be greatly appreciated!
The fact that such a simple piece of AppleScript code has no straightforward JXA translation is a testament to the sorry state of JXA and macOS automation based on OSA scripting in general:
foo's excellent answer contains helpful background information.
Another pointer is that the last time release notes were published was for OS X 10.11 (El Capitan) (as of this writing, we're on the verge of macOS 10.13 (High Sierra's) release).
This third-party July 2017 blog post announces more broadly that "that question was finally answered at WWDC last month: Apple has abandoned its automation technologies, leaving them to wither and die."
As your own example suggests, among the two dying automation scripting languages AppleScript - despite all its warts - is the more mature, reliable choice.
To solve your problem in JXA, it looks like you've come up with the best approach yourself.
Let me package it as a helper function that perhaps easies the pain somewhat - to be clear: such a helper function should NOT be necessary:
// Helper function: Given a Finder window, returns its folder's POSIX path.
// Note: No need for an ObjC.import() statement, because NSURL is
// a Foundation class, and all Foundation classes are implicitly
// available.
function posixPath(finderWin) {
return $.NSURL.alloc.initWithString(finderWin.target.url()).fileSystemRepresentation
}
// Get POSIX path of Finder's frontmost window:
posixPath(Application('Finder').finderWindows[0])
In theory you'd write something like:
finder.finderWindows[0].target({as:"alias"})
but this doesn't work and there's nothing in the documentation to indicate it's supported. But this is SOP for JXA which, like Apple's earlier Scripting Bridge, suffers numerous design flaws and omissions, which never have (and likely never will be) fixed.[1]
FWIW, here's how you do it in Node.js, using NodeAutomation:
$ node
> Object.assign(this,require('nodeautomation'));undefined
> const fn = app('Finder')
> var file = fn.FinderWindows[0].target({asType:k.alias}) // returns File object
> file.toString() // converts File object to POSIX path string
'/Users/jsmith/dev/nodeautomation'
(Be aware that NodeAutomation is a very low-priority project for me, given that Mac Automation looks to be pretty much on its last legs at Apple. Caveat emptor, etc. For non-trivial scripting I strongly recommend sticking to AppleScript as it's the only officially supported solution that actually works right.)
[1] For instance, another JXA limitation is that most apps' move and duplicate commands are seriously crippled cos the JXA authors forgot to implement insertion reference forms. (BTW, I reported all these problems before JXA was even released, and appscript had all this stuff solved a decade ago, so they've no excuse for not getting it right either.)
#Kymer, you said:
But this seems unnecessarily complex. Is there a nicer way to get to
the POSIX path without using Cocoa API or manual string wrangling?
You're on the right track. Here's the best method I know of. If there are better, I too would like to know about them. But, this seems to work well as fast, and works for both files and folders.
var finderApp = Application("Finder");
var itemList = finderApp.selection();
var oItem = itemList[0];
var oItemPaths = getPathInfo(oItem);
/* --- oItemPaths Object Keys ---
oItemPaths.itemClass
oItemPaths.fullPath
oItemPaths.parentPath
oItemPaths.itemName
*/
console.log(JSON.stringify(oItemPaths, undefined, 4))
function getPathInfo(pFinderItem) {
var itemClass = pFinderItem.class(); // returns "folder" if item is a folder.
var itemURL = pFinderItem.url();
var fullPath = decodeURI(itemURL).slice(7);
//--- Remove Trailing "/", if any, to handle folder item ---
var pathElem = fullPath.replace(/\/$/,"").split('/')
var itemName = pathElem.pop();
var parentPath = pathElem.join('/');
return {
itemClass: itemClass,
fullPath: fullPath,
parentPath: parentPath,
itemName: itemName
};
}
Here's a fairly simple example function that just grabs the window's target and then strips off the leading file:// from its url.
/*
pathToFrontWindow()
returns path to front Finder window
*/
function pathToFrontWindow() {
if ( finder.windows.length ) {
return decodeURI( finder.windows[0].target().url().slice(7) )
} else {
return ""
}
}
(() => {
// getFinderDirectory :: () -> String
const getFinderDirectory = () =>
Application('Finder')
.insertionLocation()
.url()
.slice(7);
return getFinderDirectory();
})();

Using NSURLSession from a Swift command line program

I'm trying to test a little proof-of-concept command line app prior to integrating it into a larger app. What I'm trying to do is download some data using NSURLSession using this example. However it appears that if I use the examples given in a simple OS X command line app then the app exits prior to the data being retrieved.
How can I download data from a stand-alone command line app using NSURLSession? What I've read about is using NSRunLoop however I've not yet found a clear example in Swift so if NSRunLoop is actually the way to go then any examples would be appreciated.
Any other strategies for downloading data from a URL for a Swift command line app is also welcome (infinite while loop?).
You can use a semaphore to block the current thread and wait for your URL session to finish.
Create the semaphore, kick off your URL session, then wait on the semaphore. From your URL session completion callback, signal the semaphore.
You could use a global flag (declare a volatile boolean variable) and poll that from a while loop, but that is less optimal. For one thing, you're burning CPU cycles unnecessarily.
Here's a quick example I did using a playground:
import Foundation
var sema = DispatchSemaphore( value: 0 )
class Delegate : NSObject, URLSessionDataDelegate
{
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
{
print("got data \(String(data: data, encoding: .utf8 ) ?? "<empty>")");
sema.signal()
}
}
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config, delegate: Delegate(), delegateQueue: nil )
guard let url = URL( string:"http://apple.com" ) else { fatalError("Could not create URL object") }
session.dataTask( with: url ).resume()
sema.wait()
Try this
let sema = DispatchSemaphore( value: 0)
let url = URL(string: "https://upload.wikimedia.org/wikipedia/commons/4/4d/Cat_November_2010-1a.jpg")!;
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
print("after image is downloaded");
sema.signal(); // signals the process to continue
};
task.resume();
sema.wait(); // sets the process to wait
For proof of concept(s) or tryouts/testing purposes, you can simplify asynchronous complexity by hard coding some timeout period until your stuff finishes. (see notes below)
SWIFT 5
//...your magic here
// add a little 🤓iness to make it fun at least...
RunLoop.main.run(until: Date() + 0x10) //oh boi, default init && hex craze 🤗
// yeah, 16 seconds timeout
// or even worse (!)
RunLoop.main.run(until: .distantFuture)
SWIFT 3 or earlier
//...your stuff here
RunLoop.main.run(until: Date(timeIntervalSinceNow: 15)) //will execute things on main loop for 15 seconds
NOTES :
DO NOT USE THIS IN PRODUCTION
respect the first rule
This is very quick and dirty way to overcome serious concerns of parallelism. Explore better and more complex solutions described in other answers of this question.

OSX 10.10 file change event from dispatch queue

I try to monitor file changes on OSX 10.10, starting with a fresh Cocoa application in Xcode, just adding the following code.
If I uncomment the last line in the snippet then I receive the file change events perfectly fine. But I can not make this last call because it should be a Cocoa GUI application.
I digged through a lot of documentation and can't find my error. Do I have to initialize or start this whole dispatch subsystem somehow?
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
int fd = open("<FILENAME>", O_EVTONLY);
if (fd == -1) return;
dispatch_queue_t qu = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
if (!qu) {
printf("can not get queue");
return;
}
unsigned long mask =
DISPATCH_VNODE_DELETE |
DISPATCH_VNODE_WRITE |
DISPATCH_VNODE_EXTEND |
DISPATCH_VNODE_ATTRIB |
DISPATCH_VNODE_LINK |
DISPATCH_VNODE_RENAME |
DISPATCH_VNODE_REVOKE;
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fd, mask, qu);
printf("source created\n");
if (!source) {
close(fd);
return;
}
printf("source valid\n");
dispatch_source_set_event_handler(source, ^{
printf("FILE CHANGED\n");
});
dispatch_resume(source);
printf("source resumed\n");
// If I call dispatch_main() I will receive the file system events as expected.
// But as a Cocoa application, I must not call this.
// Instead, I was under the impression that NSApplicationMain handles this.
//dispatch_main();
}
Grand Central Dispatch objects, such as dispatch sources, are automatically retained and released by ARC in recent versions of the compiler and frameworks.
At the end of your method, the last strong reference to source is lost and ARC is issuing an automatic dispatch_release(source). (It would also release the queue, but the source has another strong reference to that. So, if the source survived, so would the queue.)
You need to keep a strong reference to the source in an instance variable.

Searching Serial Ports with IOKit - Swift

I'm trying to understand IOKit and how it allows me to access serial ports in a Swift program.
The class I'm manipulating at the moment is as follows:
import Foundation
import Cocoa
import IOKit.serial.IOSerialKeys
class Serial {
init() {
}
#IBOutlet var serialListPullDown : NSPopUpButton!
func refreshSerialList(defaultprompt: String) {
// remove everything from the pull down list
serialListPullDown?.removeAllItems()
// ask for all the serial ports
IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching(kIOSerialBSDServiceValue), io_iterator_t)
}
}
Based on what I've read I think I've setup IOServiceMatchingServices correctly but I'm getting several errors such as "Expected member name or constructor call after type name" and "'o_iterator_t.Type' is not convertible to 'UnsafeMutualPointer'"
What does this mean?
A few different issues going on in there -- let's get your imports squared away first. It looks like you need these two:
import IOKit
import IOKit.serial
For the parameters, it'll be easier to see what we're working with if we define them as local variables, like so:
// this one's easy, just grabbing a constant from IOKit.serial
let masterPort: mach_port_t = kIOMasterPortDefault
// here we get back an Unmanaged<CFMutableDictionary> and need to convert it to
// a CFDictionary using takeRetainedValue()
let classesToMatch: CFDictionary = IOServiceMatching(kIOSerialBSDServiceValue).takeRetainedValue()
// the iterator that will contain the results of IOServiceGetMatchingServices
var matchingServices: io_iterator_t = 0
And lastly, you call the method:
// note that the last parameter is an UnsafeMutablePointer<io_iterator_t>
// so we need to prefix matchingServices with an ampersand (&)
let kernResult = IOServiceGetMatchingServices(masterPort, classesToMatch, &matchingServices)
if kernResult == KERN_SUCCESS {
// success
} else {
// error
}
This feels pretty near the edge of what Swift can handle right now -- definitely read these two pages well before going further:
Interacting with C APIs (Pointers section)
Working with Cocoa Data Types (Unmanaged Objects section)
Lastly, make sure you can get into the converted Swift declarations for the IOKit framework. There are a lot of useful comments and you'll be able to see which parameters and return values are unmanaged or pointers (since I don't think this framework's official documentation has been updated yet).

Swift function swizzling / runtime

Before Swift, in Objective-C I would swizzle or hook methods in a class using <objc/runtime.h>.
If anyone has any info on the topic of modifying Swift's runtime and hooking functions like CydiaSubstrate and other libraries that helped in this area, please inform me.
I've succeed with method swizzling in Swift. This example shows how to hook description method on NSDictionary
My implementation:
extension NSDictionary {
func myDescription() -> String!{
println("Description hooked")
return "Hooooked " + myDescription();
}
}
Swizzling code:
func swizzleEmAll() {
var dict:NSDictionary = ["SuperSecret": kSecValueRef]
var method: Method = class_getInstanceMethod(object_getClass(dict), Selector.convertFromStringLiteral("description"))
println(dict.description) // Check original description
var swizzledMethod: Method = class_getInstanceMethod(object_getClass(dict), Selector.convertFromStringLiteral("myDescription"))
method_exchangeImplementations(method, swizzledMethod)
println(dict.description) //Check that swizzling works
}
Edited:
This code will work for any custom Swift class that inherits from NSObject (but will not work for classes that don't.) More examples - https://github.com/mbazaliy/MBSwizzler
You would likely be able to swizzle swift-generated classes that inherit from Objective-C classes with no problem, since they appear to use dynamic method dispatch all the time. You may be able to swizzle methods of swift-defined classes that exist in the Objective-C runtime by virtue of being passed across the bridge, but the Objective-C side methods are likely to just be proxies back across the bridge to the swift-side runtime, so it's not clear that it'd be particularly helpful to swizzle them.
"Pure" swift method calls do not appear to be dispatched dynamically via anything like objc_msgSend and it appears (from brief experimentation) that the type safety of swift is implemented at compile time, and that much of the actual type information is absent (i.e. gone) at runtime for non-class types (both of which likely contribute to the purported speed advantages of swift.)
For these reasons, I expect that meaningfully swizzling swift-only methods will be significantly harder than swizzling Objective-C methods, and will probably look a lot more like mach_override than Objective-C method swizzling.
I'm answering this question more than one year later because none of the other answers provide the definitive set of requirements for method swizzling for every kind of class.
What is described by other, while it will work flawlessly for extensions to foundation/uikit classes (like NSDictionary), will simply never work for your own Swift classes.
As described here, there is an additional requirement for method swizzling other than extending NSObject in your custom class.
The swift method you want to swizzle must be marked dynamic.
If you don't mark it, the runtime will simply continue to call the original method instead of the swizzled one, even if the method pointers appear to have been swapped correctly.
Update:
I've expanded this answer in a blog post.
I had a Xcode 7 iOS project written in Swift 2, using Cocoapods. In a specific Cocoapod, with Objective-C source, I wanted to override a short method, without forking the pod. Writing a Swift extension wouldn't work in my case.
For using method swizzling, I created a new Objective-C class in my main bundle with the method I wanted to replace/inject into the cocoapod. (Also added the bridging header)
Using mbazaliy 's solution on stackflow, I put my code similar to this into the didFinishLaunchingWithOptions in my Appdelegate:
let mySelector: Selector = "nameOfMethodToReplace"
let method: Method = class_getInstanceMethod(SomeClassInAPod.self, mySelector)
let swizzledMethod: Method = class_getInstanceMethod(SomeOtherClass.self, mySelector)
method_exchangeImplementations(method, swizzledMethod)
This worked perfectly. The difference between #mbazaliy 's code is that I didn't need to create an instance of the SomeClassInAPod class first, which in my case would have been impossible.
Note: I put the code in the Appdelegate because every other time the code runs, it exchanges the method for the original - it should only run one time.
I also needed to copy some assets that were referenced in the Pod's bundle to the main bundle.
I wouldn't do it that way, I think closures provide the answers (as they give you a chance to intercept, evaluate, and forward the invocation of the function, additionally it will be easy to extend when and if we have reflection.
http://www.swift-studies.com/blog/2014/7/13/method-swizzling-in-swift
I would like to extend the great answer provided by mbazaliy.
Another way of doing swizzling in Swift is by providing an implementation using an Objective-C block.
e.g. to replace descriptionmethod on class NSString we can write:
let originalMethod = class_getInstanceMethod(NSString.self, "description")
let impBlock : #objc_block () -> NSString =
{ () in return "Bit of a hack job!" }
let newMethodImp = imp_implementationWithBlock(unsafeBitCast(impBlock, AnyObject.self))
method_setImplementation(originalMethod, newMethodImp)
This works as of Swift 1.1.
A safe, easy, powerful and efficient hook framework for iOS (Support Swift and Objective-C). https://github.com/623637646/SwiftHook
For example, this is your class
class MyObject {
#objc dynamic func noArgsNoReturnFunc() {
}
#objc dynamic func sumFunc(a: Int, b: Int) -> Int {
return a + b
}
#objc dynamic class func classMethodNoArgsNoReturnFunc() {
}
}
The key words of methods #objc and dynamic are necessary
The class doesn't have to inherit from NSObject. If the class is written by Objective-C, Just hook it without any more effort
Perform the hook closure before executing specified instance's method.
let object = MyObject()
let token = try? hookBefore(object: object, selector: #selector(MyObject.noArgsNoReturnFunc)) {
// run your code
print("hooked!")
}
object.noArgsNoReturnFunc()
token?.cancelHook() // cancel the hook
Perform the hook closure after executing specified instance's method. And get the parameters.
let object = MyObject()
let token = try? hookAfter(object: object, selector: #selector(MyObject.sumFunc(a:b:)), closure: { a, b in
// get the arguments of the function
print("arg1 is \(a)") // arg1 is 3
print("arg2 is \(b)") // arg2 is 4
} as #convention(block) (Int, Int) -> Void)
_ = object.sumFunc(a: 3, b: 4)
token?.cancelHook() // cancel the hook
The key word #convention(block) is necessary
For hook at before and after. The closure's args have to be empty or the same as method. The return type has to be void
Totally override the mehtod for specified instance. You can call original with the same parameters or different parameters. Don't even call the original method if you want.
let object = MyObject()
let token = try? hookInstead(object: object, selector: #selector(MyObject.sumFunc(a:b:)), closure: { original, a, b in
// get the arguments of the function
print("arg1 is \(a)") // arg1 is 3
print("arg2 is \(b)") // arg2 is 4
// run original function
let result = original(a, b) // Or change the parameters: let result = original(-1, -2)
print("original result is \(result)") // result = 7
return 9
} as #convention(block) ((Int, Int) -> Int, Int, Int) -> Int)
let result = object.sumFunc(a: 3, b: 4) // result
print("hooked result is \(result)") // result = 9
token?.cancelHook() // cancel the hook
For hook with instead. The closure's first argument has to be a closure which has the same types with the method. The rest args and return type have to be the same as the method.
Perform the hook closure before executing the method of all instances of the class.
let token = try? hookBefore(targetClass: MyObject.self, selector: #selector(MyObject.noArgsNoReturnFunc)) {
// run your code
print("hooked!")
}
MyObject().noArgsNoReturnFunc()
token?.cancelHook() // cancel the hook
Perform the hook closure before executing the class method.
let token = try? hookClassMethodBefore(targetClass: MyObject.self, selector: #selector(MyObject.classMethodNoArgsNoReturnFunc)) {
// run your code
print("hooked!")
}
MyObject.classMethodNoArgsNoReturnFunc()
token?.cancelHook() // cancel the hook
After spending some time on it... Wake up this morning.... beta 6 is out and
Problem Fixed in beta6!
From release notes
"Dynamic dispatch can now call overrides of methods and properties introduced in class extensions, fixing a regression introduced in Xcode 6 beta 5. (17985819)!"

Resources