I'm also not aware of any existing project that allows calling Swift from Python. As others have said above, the other direction (calling Python from Swift) is quite well-supported, but that doesn't help us much here.
In principle, it is entirely possible to call Swift from Python. It would require more work than with Objective-C though.
Objective-C's calling conventions are completely compatible with C, which allows using the standard
ctypeslibrary to call Objective-C code. The Objective-C API also exposes a lot of information at runtime, which allows inferring method return/argument types so you don't have to set them manually in most cases.
With Swift the situation is quite different. As far as I know, Swift itself does not provide nearly as many runtime API functions as Objective-C does, so there is no easy way to get function/method type information at runtime. Swift might not even store this type information in the compiled binaries at all (I haven't checked recently).
There's also the more fundamental problem that Swift's type system and calling convention are completely different from C. In some simple cases the calling conventions overlap enough that you can call some Swift functions using
ctypes, but IIRC all instance methods (anything with a
self) and functions/methods with certain complex return types (tuples, etc.) are incompatible with the C calling convention.
This means that calling Swift from Python would require a custom extension module similar to
ctypes, but for the Swift calling convention. Internally,
ctypesuses the libffi library, but libffi does not support the Swift ABI (yet?). In any case, since a custom native module is required, a library like this wouldn't be usable in Pythonista unless @omz compiles and includes it in the app.
TLDR: It would be possible, but would require quite a bit of work, and couldn't be developed in Pythonista.
If I'm reading the error message correctly,
rubicon.objcis installed properly. The error about
ctypes.macholibis a known issue in Pythonista: https://github.com/omz/Pythonista-Issues/issues/311
Sadly it hasn't been fixed yet, but there is a workaround - you can copy the missing files from the CPython source code. See this comment on the issue for instructions: https://github.com/omz/Pythonista-Issues/issues/311#issuecomment-398715503
The second argument of
dispatch_asyncneeds to be a block object. You can't pass in a normal Python function, because
ctypeswon't know how to convert it. Instead, you have to create the block object manually:
def block_code(_self): print("ok") block = ObjCBlock(block_code, None, [c_void_p]) dispatch_async(dispatch_get_main_queue(), byref(block._as_parameter_))
For more details about how
ObjCBlockworks, you can look at the
ObjCBlocksection of the
By the way,
dispatch_asyncdoesn't return anything, so you should set its
c_void_p. (If you use
c_void_pit will probably still work, but the return value won't be anything useful.)
dispatch_get_main_queueis not a normal C function, so you can't access it normally via
objc_util. Instead, you need to reproduce
dispatch_get_main_queue's functionality in Python using some custom code:
from ctypes import Structure, byref, cast, c_void_p from objc_util import ObjCInstance, c class struct_dispatch_queue_s(Structure): pass _dispatch_main_q = struct_dispatch_queue_s.in_dll(c, "_dispatch_main_q") def dispatch_get_main_queue(): return ObjCInstance(cast(byref(_dispatch_main_q), c_void_p))
You don't need to understand what this code does exactly - you can just copy-paste it into your code and use
dispatch_get_main_queue()like you would normally.
(If anyone is interested in the exact details of what the above code does and where it comes from, see this page. The original C code is very complex, so you need to be familiar with both
ctypesand the C preprocessor to understand all the translation steps. But again, you don't have to understand all of this - you can just copy-paste and use the finished Python code.)
@shinyformica Correct - these string constants are actually global variables of type
NSString *, which means that they contain a pointer to the actual string object.
To read the pointer from the global variable, you need to use
ctypes.c_void_p.in_dll(objc_util.c, "ConstantName"), as @mikael already found above.
objc_util.c.ConstantNamewill "work" (it doesn't crash), but that treats
ConstantNameas a function and not a variable, so it returns a function pointer (which will crash when you try to call it, because normal variables are not valid functions).
The value returned by
in_dllis a regular Objective-C object pointer (which points to the
NSStringobject), but as a
c_void_pobject, so you need to cast it to
ObjCInstancemanually so that you can use it. Once you do that, you have a regular
NSStringobject with the value you're looking for.
>>> objc_util.ObjCInstance(ctypes.c_void_p.in_dll(objc_util.c, 'UICollectionElementKindSectionHeader')) <b'__NSCFConstantString': UICollectionElementKindSectionHeader> >>> objc_util.ObjCInstance(ctypes.c_void_p.in_dll(objc_util.c, 'UICollectionElementKindSectionFooter')) <b'__NSCFConstantString': UICollectionElementKindSectionFooter>
Strangely, the value of both string constants is literally their name, so the approach suggested by @mikael (just pass the name of the constant as a string) should have worked. Maybe UIKit expects you to pass in the exact pointer, and not just any string with the same contents? (That would be quite strange though.)
(By the way, please do @-mention me if there's something where you think I can help. Sometimes I only skim the forums very quickly, so I might miss some questions by accident.)
This is probably not possible.
In general, iOS does not allow one app to control another app. Apps can provide Siri shortcuts or a URL scheme to allow limited control by other apps, but this has to be specifically supported by the app, and the developer decides what actions can be triggered by other apps.
If the Sonos app provides Siri shortcuts or a URL scheme to control music playback, you can use that, otherwise you're out of luck. It doesn't make a difference whether you use Pythonista or the Shortcuts app to do the automation - both apps only have access to the control endpoints provided by the other app, and nothing more.
From an Objective-C perspective,
CFURLRefis basically identical to
NSURL *, because of "toll-free bridging" between Core Foundation and Objective-C/Foundation. (This also applies to other CF types, like
NSArray *, etc.) So you should be able to create a
NSURLfrom a string normally (using
objc_util.nsurl) and pass the
ObjCInstanceinto the parameter that expects a
@mikael I'm not very creative with these things - do you have some example code based on
objc_utilthat I could translate to Rubicon?
In general the syntax for classes looks like this:
from rubicon.objc import NSInteger, NSObject, ObjCProtocol, objc_method WhateverDelegate = ObjCProtocol("WhateverDelegate") class MyCustomDelegate(NSObject, protocols=[WhateverDelegate]): # the protocols=[...] part is optional of course, if you don't need to implement any protocol @objc_method def someThing_didAThingWithCount_(self, something, count: NSInteger) -> None: print(something, count) something.delegate = MyCustomDelegate.new()
The Python 3 type annotation syntax is used to set each method's return and argument types. You use regular
ctypestype objects for the types, and Rubicon predefines some common typedefs like
NSInteger. Anything you don't annotate is assumed to be an Objective-C object (
Blocks are similar:
from rubicon.objc import Block, NSInteger @Block def thing_callback(number: NSInteger) -> None: print(number) whatever.doThing("abc", withCallback=callback)