How to listen to global hotkeys with Swift in a macOS app? - swift

I'm trying to have a handler in my Mac OS X app written in Swift for a global (system-wide) hotkey combo but I just cannot find proper documentation for it. I've read that I'd have to mess around in some legacy Carbon API for it, is there no better way? Can you show me some proof of concept Swift code? Thanks in advance!

Since Swift 2.0, you can now pass a function pointer to C APIs.
var gMyHotKeyID = EventHotKeyID()
gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
gMyHotKeyID.id = UInt32(keyCode)
var eventType = EventTypeSpec()
eventType.eventClass = OSType(kEventClassKeyboard)
eventType.eventKind = OSType(kEventHotKeyPressed)
// Install handler.
InstallEventHandler(GetApplicationEventTarget(), {(nextHanlder, theEvent, userData) -> OSStatus in
var hkCom = EventHotKeyID()
GetEventParameter(theEvent, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, sizeof(EventHotKeyID), nil, &hkCom)
// Check that hkCom in indeed your hotkey ID and handle it.
}, 1, &eventType, nil, nil)
// Register hotkey.
let status = RegisterEventHotKey(UInt32(keyCode), UInt32(modifierKeys), gMyHotKeyID, GetApplicationEventTarget(), 0, &hotKeyRef)

I don't believe you can do this in 100% Swift today. You'll need to call InstallEventHandler() or CGEventTapCreate(), and both of those require a CFunctionPointer, which can't be created in Swift. Your best plan is to use established ObjC solutions such as DDHotKey and bridge to Swift.
You can try using NSEvent.addGlobalMonitorForEventsMatchingMask(handler:), but that only makes copies of events. You can't consume them. That means the hotkey will also be passed along to the currently active app, which can cause problems. Here's an example, but I recommend the ObjC approach; it's almost certainly going to work better.
let keycode = UInt16(kVK_ANSI_X)
let keymask: NSEventModifierFlags = .CommandKeyMask | .AlternateKeyMask | .ControlKeyMask
func handler(event: NSEvent!) {
if event.keyCode == self.keycode &&
event.modifierFlags & self.keymask == self.keymask {
println("PRESSED")
}
}
// ... to set it up ...
let options = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionaryRef
let trusted = AXIsProcessTrustedWithOptions(options)
if (trusted) {
NSEvent.addGlobalMonitorForEventsMatchingMask(.KeyDownMask, handler: self.handler)
}
This also requires that accessibility services be approved for this app. It also doesn't capture events that are sent to your own application, so you have to either capture them with your responder chain, our use addLocalMointorForEventsMatchingMask(handler:) to add a local handler.

The following code works for me for Swift 5.0.1. This solution is the combination of the solution from the accepted answer by Charlie Monroe and the recommendation by Rob Napier to use DDHotKey.
DDHotKey seems to work out of the box but it had one limitation that I had to change: the eventKind is hardcoded to kEventHotKeyReleased while I needed both kEventHotKeyPressed and kEventHotKeyReleased event types.
eventSpec.eventKind = kEventHotKeyReleased;
If you want to handle both Pressed and Released events, just add a second InstallEventHandler call which registers the other event kind.
This the complete example of the code that registers the "Command + R" key for the kEventHotKeyReleased type.
import Carbon
extension String {
/// This converts string to UInt as a fourCharCode
public var fourCharCodeValue: Int {
var result: Int = 0
if let data = self.data(using: String.Encoding.macOSRoman) {
data.withUnsafeBytes({ (rawBytes) in
let bytes = rawBytes.bindMemory(to: UInt8.self)
for i in 0 ..< data.count {
result = result << 8 + Int(bytes[i])
}
})
}
return result
}
}
class HotkeySolution {
static
func getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags) -> UInt32 {
let flags = cocoaFlags.rawValue
var newFlags: Int = 0
if ((flags & NSEvent.ModifierFlags.control.rawValue) > 0) {
newFlags |= controlKey
}
if ((flags & NSEvent.ModifierFlags.command.rawValue) > 0) {
newFlags |= cmdKey
}
if ((flags & NSEvent.ModifierFlags.shift.rawValue) > 0) {
newFlags |= shiftKey;
}
if ((flags & NSEvent.ModifierFlags.option.rawValue) > 0) {
newFlags |= optionKey
}
if ((flags & NSEvent.ModifierFlags.capsLock.rawValue) > 0) {
newFlags |= alphaLock
}
return UInt32(newFlags);
}
static func register() {
var hotKeyRef: EventHotKeyRef?
let modifierFlags: UInt32 =
getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags.command)
let keyCode = kVK_ANSI_R
var gMyHotKeyID = EventHotKeyID()
gMyHotKeyID.id = UInt32(keyCode)
// Not sure what "swat" vs "htk1" do.
gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
// gMyHotKeyID.signature = OSType("htk1".fourCharCodeValue)
var eventType = EventTypeSpec()
eventType.eventClass = OSType(kEventClassKeyboard)
eventType.eventKind = OSType(kEventHotKeyReleased)
// Install handler.
InstallEventHandler(GetApplicationEventTarget(), {
(nextHanlder, theEvent, userData) -> OSStatus in
// var hkCom = EventHotKeyID()
// GetEventParameter(theEvent,
// EventParamName(kEventParamDirectObject),
// EventParamType(typeEventHotKeyID),
// nil,
// MemoryLayout<EventHotKeyID>.size,
// nil,
// &hkCom)
NSLog("Command + R Released!")
return noErr
/// Check that hkCom in indeed your hotkey ID and handle it.
}, 1, &eventType, nil, nil)
// Register hotkey.
let status = RegisterEventHotKey(UInt32(keyCode),
modifierFlags,
gMyHotKeyID,
GetApplicationEventTarget(),
0,
&hotKeyRef)
assert(status == noErr)
}
}

A quick Swift 3 update for the setup:
let opts = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionary
guard AXIsProcessTrustedWithOptions(opts) == true else { return }
NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: self.handler)

I maintain this Swift package that makes it easy to both add global keyboard shortcuts to your app and also let the user set their own.
import SwiftUI
import KeyboardShortcuts
// Declare the shortcut for strongly-typed access.
extension KeyboardShortcuts.Name {
static let toggleUnicornMode = Self("toggleUnicornMode")
}
#main
struct YourApp: App {
#StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
// …
}
Settings {
SettingsScreen()
}
}
}
#MainActor
final class AppState: ObservableObject {
init() {
// Register the listener.
KeyboardShortcuts.onKeyUp(for: .toggleUnicornMode) { [self] in
isUnicornMode.toggle()
}
}
}
// Present a view where the user can set the shortcut they want.
struct SettingsScreen: View {
var body: some View {
Form {
HStack(alignment: .firstTextBaseline) {
Text("Toggle Unicorn Mode:")
KeyboardShortcuts.Recorder(for: .toggleUnicornMode)
}
}
}
}
SwiftUI is used in this example, but it also supports Cocoa.

Take a look at the HotKey Library. You can simply use Carthage to implement it into your own app.
HotKey Library

there is a pretty hacky, but also pretty simple workaround if your app has a Menu:
add a new MenuItem (maybe call it something like "Dummy for Hotkey")
in the attributes inspector, conveniently enter your hotkey in the Key Equivalent field
set Allowed when Hidden, Enabled and Hidden to true
link it with an IBAction to do whatever your hotkey is supposed to do
done!

Related

AudioKit v5 - What is the best way to program polyphony?

I am trying to use AudioKit v5 to build a simple synthesizer app that plays a certain frequency whenever a button is pressed.
I would like there to be 28 buttons. However, I do not know if I should use the DunneAudioKit Synth class or create a dictionary of 28 AudioKit DynamicOscillators.
If I use the Synth class, I currently have no way of changing the waveform of the synth. If I use the dictionary of DynamicOscillators, I will have to start 28 oscillators and keep them running throughout the lifetime of the app. Neither scenario seems that great. One option only allows for a certain sound while the other one is energy inefficient.
Is there a better way to allow for polyphony using AudioKit? A way that is efficient and also able to produce many different kinds of sound? AudioKit SynthOne is a great example of what I am trying to achieve.
I downloaded "AudioKit Synth One - The Ultimate Guide" by Francis Preve and from that I learned that SynthOne uses 2 Oscillators, a Sub-Oscillator, an FM Pair, and a Noise Generator to produce its sounds. However, the eBook does not explain how to actually code a polyphonic synthesizer using these 5 generators. I know that SynthOne's source code is online. I have downloaded it, but it is a little too advanced for me to understand. However, if someone can help explain how to use just those 5 objects to create a polyphonic synthesizer, that would be incredible.
Thanks in advance.
I'm not sure how they did things in AudioKit 4 which Synth One uses. I would speculate that it has an internal oscillator array bank when polyphonic mode is enabled. So essentially one instance of an oscillator per voice.
In the AudioKit 5 documentation it says Dunne Synth is the only polyphonic oscillator at this time, but I did added a WIP polyphonic oscillator example in the AudioKit Cookbook. I'm not sure how much of a resource hog it is. 28 instances seems excessive so you might be able to get by with around 10 and change the frequencies for each voice with button presses.
The third option would be to use something like AppleSampler or DunneSampler and make instruments based on single cycle wavetable audio files. This is more of a workaround and wouldn't give as much control over certain parameters, but it would be lighter on the resources.
I had a similar question and tried several ways of making a versatile polyphonic sampler.
It's true that the AppleSampler and the DunneSampler support polyphone; however, I needed a sampler that I could control with more precision on a note-by-note bases; i.e. playing each "voice" with unique playback parameters like playspeed, etc.
I found that building a sampler based on the AudioPlayer was the right path for me; and there, I created a member variable inside my sampler "voice" that kept track of when that voice was "busy"; when a "voice" is assigned a note to play, it marks itself as "busy", and when it's done, the callback from the AudioPlayer executes a function that sets the voice's "busy" variable to "false".
I then use a "conductor" to find the first available voice that is not "busy" to play a sound.
Here is a snippet:
import AudioKit
import AudioKitUI
import AVFoundation
import Keyboard
import Combine
import SwiftUI
import DunneAudioKit
class AudioPlayerVoice: ObservableObject, HasAudioEngine {
// For audio playback
let engine = AudioEngine()
let player = AudioPlayer()
let variSpeed: VariSpeed
var voiceNumber = 0
var busy : Bool
init() {
variSpeed = VariSpeed(player)
engine.output = variSpeed
do {
try engine.start()
} catch {
Log("AudioKit did not start!")
}
busy = false
variSpeed.rate = 1.0
player.isBuffered = true
player.completionHandler = donePlaying
}
func play(buffer: AVAudioPCMBuffer) {
// Set this voice to busy so that new incoming notes are not palyed here
busy = true
// Load buffer into player
player.load(buffer: buffer)
// Compare buffer and audioplayer formats
// print("Player format 1: ")
// print(player.outputFormat)
// print("Buffer format: ")
// print(buffer.format)
// Set AudioPlayer format to be the same as buffer format
player.playerNode.engine?.connect( player.playerNode, to: player.mixerNode, format: buffer.format)
// Compare buffer and audioplayer formats again to see if the above line changed anything
// print("Player format 2: ")
// print(player.outputFormat)
// Play sound with a completion callback
player.play(completionCallbackType: .dataPlayedBack)
}
func donePlaying() {
print("done!")
busy = false
}
}
class AudioPlayerConductor: ObservableObject {
// Mark Published so View updates label on changes
#Published private(set) var lastPlayed: String = "None"
let voiceCount = 16
var soundFileList: [String] = []
var buffers : [AVAudioPCMBuffer] = []
var players: [AudioPlayerVoice] = []
var sampleDict: [String: AVAudioPCMBuffer] = [:]
func loadAudioFiles() {
// Build audio file name list
let fileNameExtension = ".wav"
if let files = try? FileManager.default.contentsOfDirectory(atPath: Bundle.main.bundlePath + "/Samples" ){
// var counter = 0
///print("Files... " + files)
for file in files {
if file.hasSuffix(fileNameExtension) {
let name = file.prefix(file.count - fileNameExtension.count)
// add sound file name without extension to our soundFileist
soundFileList.append(String(name))
// get url for current sound
let url = Bundle.main.url(forResource: String(name), withExtension: "wav", subdirectory: "Samples")
// read audiofile into an AVAudioFile
let audioFile = try! AVAudioFile(forReading: url!)
// find the audio format and frame count
let audioFormat = audioFile.processingFormat
let audioFrameCount = UInt32(audioFile.length)
// create a new AVAudioPCMBuffer and read from the AVAudioFile into the AVAudioPCMBuffer
let audioFileBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: audioFrameCount)
try! audioFile.read(into: audioFileBuffer!)
// updated the sampleDict dictionary with "name" / "buffer" key / value
sampleDict[String(name)] = audioFileBuffer
//print("loading... " + name)
//print(".......... " + url!.absoluteString)
}
}
}
print("Loaded Samples:")
print(soundFileList)
}
func initializeSamplerVoices() {
for i in 1...voiceCount {
let newAudioPlayerVoice = AudioPlayerVoice()
newAudioPlayerVoice.voiceNumber = i
players.append(newAudioPlayerVoice)
}
}
func playWithAvailableVoice (bufferToPlay: AVAudioPCMBuffer, playspeed: Float) {
for i in 0...(voiceCount-1) {
if (!players[i].busy) {
players[i].variSpeed.rate = playspeed
players[i].play(buffer: bufferToPlay)
break
}
}
}
func playXY(x: Double, y: Double) {
let playspeed = Float(AliSwift.scale(x, 0.0, UIScreen.screenWidth, 0.1, 3.0))
let soundNumber = Int(AliSwift.scale(y, 0.0, UIScreen.screenHeight, 0 , Double(soundFileList.count - 1)))
let soundBuffer = sampleDict[soundFileList[soundNumber]]
playWithAvailableVoice(bufferToPlay: soundBuffer!, playspeed: playspeed)
}
init() {
loadAudioFiles()
initializeSamplerVoices()
}
}
struct ContentViewAudioPlayer: View {
#StateObject var conductor = AudioPlayerConductor()
// #StateObject var samplerVoice = AudioPlayerVoice()
var body: some View {
ZStack {
VStack {
Rectangle()
.fill(.red)
.frame(maxWidth: .infinity)
.frame(maxHeight: .infinity)
.onTapGesture { location in
print("Tapped at \(location)")
let someSound = conductor.sampleDict.randomElement()!
let someSoundName = someSound.key
let someSoundBuffer = someSound.value
print("Playing: " + someSoundName)
conductor.playXY(x: location.x, y: location.y)
}
}
.onAppear {
// conductor.start()
}
.onDisappear {
// conductor.stop()
}
}
}
struct ContentViewAudioPlayer_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Coredata/swift : hasChanges not as expected

This function is not acting as expected. I'm trying to nil a set of fields.The earlier section gets the correct field names, and is used in other functions. I've got about ten tables, and they all share the same context, in case that matters.
The first unexpected thing is that "yes, changes" never runs, so I presume that the settings object is detached from its context. Or perhaps CoreData treats nil as some kind of exception to triggering the .hasChanges flag?
When it runs, the save throws no errors, and the object displays as expected, displayed with the values set to nil. But there are no changes in the db.
I can save data into these fields without problem, and confirm that in the db; this problem only happens with setting the value to nil.
static func clearSettings(_ s : Set<PaletteElementType>? = nil) {
guard var setting = activeSetting() else {return}
print(setting.id)
let cats = s ?? PaletteView.compCatButtons
let tgts = setting.getAssociatedFieldNames(tgts: cats, clearing : true, values: false)
for (key, val) in tgts {
var src = Set((val as? Dictionary<FieldNameSuffixes, String>)!.values)
if key == .catBgndButton {
src = src.union(["opacity", "opacityStart", "opacityStartDelta","opacityEnd", "opacityEndDelta", "opacityTimer"])
}
for s in src {
print(s)
setting.setNilValueForKey(s)
if Blocks.context!.hasChanges {
print("yes, changes")
}
do {
try Blocks.context!.save()
print("deleted \(setting.value(forKey: s))")
} catch { print("deadly dogs")}
}
print("val is \(setting)")
}
}
OK, working when I do it this way:
static func clearSettings(_ s : Set<PaletteElementType>? = nil) {
guard var setting = activeSetting() else {return}
print(setting.id)
let cats = s ?? PaletteView.compCatButtons
let tgts = setting.getAssociatedFieldNames(tgts: cats, clearing : true, values: false)
for (key, val) in tgts {
var src = Set((val as? Dictionary<FieldNameSuffixes, String>)!.values)
if key == .catBgndButton {
src = src.union(["opacity", "opacityStart", "opacityStartDelta","opacityEnd", "opacityEndDelta", "opacityTimer"])
}
for n in setting.entity.attributesByName.enumerated() {
if src.contains( n.element.key as String) {
print("found one")
setting.setNilValueForKey(n.element.key)
}
}
do {
try Blocks.context!.save()
} catch {print("bumpy beasts")}
print("val is \(setting)")
}
}
Happy it's working, but I don't really understand the distinction here. What is the better way to handle this? I'm not chasing some super performant code, so I don't mind a few extra loops... but what's the deal?

Moving Table rows in SwiftUI

Is it possible to support moving rows in a SwiftUI Table view?
I know there's List which can optionally support selection and drag-and-drop to move single or multiple rows. Since it seems similar, I would like to do this with a Table too, but I wasn't able to find any way to do this. Is this possible in SwiftUI? And if it is, what's the best way to do it?
Where I started to figure this out was the WWDC 2021 session "SwiftUI on the Mac: Finishing Touches". I highly recommend this video, as well as the preceding one "SwiftUI on the Mac: Build the Fundamentals". The code for both sessions is available.
Since you didn't include your code to show what you want to do, I have to use my code. I have a table based on an array of an Identifiable struct called Channel. Among a number of fields which are irrelevant to this problem, there is a field "id" of type UUID.
Following the model of the WWDC video, I made an extension to Channel:
import UniformTypeIdentifiers
extension Channel {
static var draggableType = UTType(exportedAs: "com.yourCompany.yourApp.channel")
// define your own type here. don't forget to include it in your info.plist as an exported type
static func fromItemProviders(_ itemProviders: [NSItemProvider], completion: #escaping ([Channel]) -> Void) {
let typeIdentifier = Self.draggableType.identifier
let filteredProviders = itemProviders.filter {
$0.hasItemConformingToTypeIdentifier(typeIdentifier)
}
let group = DispatchGroup()
var result = [Int: Channel]()
for (index, provider) in filteredProviders.enumerated() {
group.enter()
provider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { (data, error) in
defer { group.leave() }
guard let data = data else { return }
let decoder = JSONDecoder()
guard let channel = try? decoder.decode(Channel.self, from: data)
else { return }
result[index] = channel
}
}
group.notify(queue: .global(qos: .userInitiated)) {
let channels = result.keys.sorted().compactMap { result[$0] }
DispatchQueue.main.async {
completion(channels)
}
}
}
var itemProvider: NSItemProvider {
let provider = NSItemProvider()
provider.registerDataRepresentation(forTypeIdentifier: Self.draggableType.identifier, visibility: .all) {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(self)
$0(data, nil)
} catch {
$0(nil, error)
}
return nil
}
return provider
}
}
This makes an item in the table draggable. Of course, that does no good if there's nothing that will accept the drag. So, you have to make a change to your Table.
Table(selection: $selection, sortOrder: $sortOrder) {
// for clarity, I've removed the table columns
} rows: {
ForEach(document.channels) { channel in
TableRow(channel)
.itemProvider { channel.itemProvider }
}
.onInsert(of: [Channel.draggableType]) { index, providers in
Channel.fromItemProviders(providers) { channels in
document.channels.insert(contentsOf: channels, at: newIndex)
}
}
}
}
Now that will enable you to drag item or items from one window to another. You can, of course, drag within a table now, too. Unfortunately, you will end up making a copy in the new place. Not what you want to do in most cases. How to fix this? Delete the original copy! Of course, you can also run into the problem of indexing in the right place, and if you drag more than one item (from a discontinuous selection, even worse!), the results become, shall we say, undefined.
I still wanted to be able to drag multiple items from another table, so the final onInsert becomes a little more complex (Which I'm sure could be cleaned up a bot further):
Channel.fromItemProviders(providers) { channels in
var newIndex = index
let intraTableDrag = document.channels.contains(where: {$0.id == channels[0].id})
if intraTableDrag && channels.count == 1 {
let oldIndex = document.channels.firstIndex(where: {$0.id == channels[0].id})
if newIndex > oldIndex! {
newIndex -= 1
}
for channel in channels {
let channelID = channel.id
removeChannel(withID: channelID)
}
let maxIndex = document.channels.count
if index > maxIndex {
newIndex = maxIndex
}
}
if (intraTableDrag && channels.count == 1) || !intraTableDrag {
document.channels.insert(contentsOf: channels, at: newIndex)
document.setChannelLocationToArrayOrder()
}
}
}
I hope this is enough to get you started. Good luck!

more idiomatic swift test for optionset?

Still getting used to the use of OptionSetType in Swift.
In good ol' C, if I had something like
typedef enum {
CHAR_PROP_BROADCAST =0x01,
CHAR_PROP_READ =0x02,
CHAR_PROP_WRITE_WITHOUT_RESP =0x04,
CHAR_PROP_WRITE =0x08,
CHAR_PROP_NOTIFY =0x10,
CHAR_PROP_INDICATE =0x20,
CHAR_PROP_SIGNED_WRITE =0x40,
CHAR_PROP_EXT =0x80
} CharacteristicProperty;
I could test a set of flags with something simple like:
if ((propertiesMask & (CHAR_PROP_NOTIFY | CHAR_PROP_INDICATE)) != 0) ...
The Swift alternative might look like
let properties:CBCharacteristicProperties = [.Write, .Read, .Indicate]
!properties.intersect([.Indicate, .Notify]).isEmpty
Is there a more idiomatic way to do this test? Not a fan of the ! out front. But otherwise, seems straightforward enough, except I'm really interested in when there IS an intersection. This led me to want to add my own.
extension OptionSetType {
func hasIntersection(other:Self) -> Bool {
return !self.intersect(other).isEmpty
}
}
Which then allows me to write
properties.hasIntersection([.Indicate, .Notify])
Is there a better/more idiomatic way to do this? Did I roll my own and miss something?
There's this method from the protocol SetAlgebraType which OptionSetType implements:
isDisjointWith(_: Self) -> Bool
Returns true iff self.intersect(other).isEmpty.
So you could shorten your test to:
!properties.isDisjointWith([.Indicate, .Notify])
or
properties.isDisjointWith([.Indicate, .Notify]) == false
You can also compare the raw values with bitwise operators, just as you would do in C:
(properties.rawValue & (CharacteristicProperties.Notify.rawValue | CharacteristicProperties.Indicate.rawValue)) != 0
Full example code (in a playground):
struct CBCharacteristicProperties : OptionSetType {
let rawValue: UInt
init(rawValue: UInt) { self.rawValue = rawValue }
static let Broadcast = CBCharacteristicProperties(rawValue:0x01)
static let Read = CBCharacteristicProperties(rawValue:0x02)
static let WriteWithoutResp = CBCharacteristicProperties(rawValue:0x04)
static let Write = CBCharacteristicProperties(rawValue:0x08)
static let Notify = CBCharacteristicProperties(rawValue:0x10)
static let Indicate = CBCharacteristicProperties(rawValue:0x20)
static let SignedWrite = CBCharacteristicProperties(rawValue:0x40)
static let Ext = CBCharacteristicProperties(rawValue:0x80)
}
let properties = CBCharacteristicProperties([.Write, .Read, .Indicate])
print(!properties.intersect([.Indicate, .Notify]).isEmpty)
print(!properties.isDisjointWith([.Indicate, .Notify]))
print(properties.isDisjointWith([.Indicate, .Notify]) == false)
print((properties.rawValue & (CBCharacteristicProperties.Notify.rawValue | CBCharacteristicProperties.Indicate.rawValue)) != 0)
result:
"true"
"true"
"true"
"true"
The actual solution I found most appealing in the end was to simply add the following extension:
extension SetAlgebraType {
var notEmpty:Bool {
return self.isEmpty.NOT
}
}
This allowed me to write code:
if properties.intersect([.Indicate, .Notify]).notEmpty {
...
}

How to change the value of a child from a Mirror introspection

I'm doing a bunch of BLE in iOS, which means lots of tight packed C structures being encoded/decoded as byte packets. The following playground snippets illustrate what I'm trying to do generically.
import Foundation
// THE PROBLEM
struct Thing {
var a:UInt8 = 0
var b:UInt32 = 0
var c:UInt8 = 0
}
sizeof(Thing) // --> 9 :(
var thing = Thing(a: 0x42, b: 0xDEADBEAF, c: 0x13)
var data = NSData(bytes: &thing, length: sizeof(Thing)) // --> <42000000 afbeadde 13> :(
So given a series of fields of varying size, we don't get the "tightest" packing of bytes. Pretty well known and accepted. Given my simple structs, I'd like to be able to arbitrarily encode the fields back to back with no padding or alignment stuff. Relatively easy actually:
// ARBITRARY PACKING
var mirror = Mirror(reflecting: thing)
var output:[UInt8] = []
mirror.children.forEach { (label, child) in
switch child {
case let value as UInt32:
(0...3).forEach { output.append(UInt8((value >> ($0 * 8)) & 0xFF)) }
case let value as UInt8:
output.append(value)
default:
print("Don't know how to serialize \(child.dynamicType) (field \(label))")
}
}
output.count // --> 6 :)
data = NSData(bytes: &output, length: output.count) // --> <42afbead de13> :)
Huzzah! Works as expected. Could probably add a Class around it, or maybe a Protocol extension and have a nice utility. The problem I'm up against is the reverse process:
// ARBITRARY DEPACKING
var input = output.generate()
var thing2 = Thing()
"\(thing2.a), \(thing2.b), \(thing2.c)" // --> "0, 0, 0"
mirror = Mirror(reflecting:thing2)
mirror.children.forEach { (label, child) in
switch child {
case let oldValue as UInt8:
let newValue = input.next()!
print("new value for \(label!) would be \(newValue)")
// *(&child) = newValue // HOW TO DO THIS IN SWIFT??
case let oldValue as UInt32: // do little endian
var newValue:UInt32 = 0
(0...3).forEach {
newValue |= UInt32(input.next()!) << UInt32($0 * 8)
}
print("new value for \(label!) would be \(newValue)")
// *(&child) = newValue // HOW TO DO THIS IN SWIFT??
default:
print("skipping field \(label) of type \(child.dynamicType)")
}
}
Given an unpopulated struct value, I can decode the byte stream appropriately, figure out what the new value would be for each field. What I don't know how to do is to actually update the target struct with the new value. In my example above, I show how I might do it with C, get the pointer to the original child, and then update its value with the new value. I could do it easily in Python/Smalltalk/Ruby. But I don't know how one can do that in Swift.
UPDATE
As suggested in comments, I could do something like the following:
// SPECIFIC DEPACKING
extension GeneratorType where Element == UInt8 {
mutating func _UInt8() -> UInt8 {
return self.next()!
}
mutating func _UInt32() -> UInt32 {
var result:UInt32 = 0
(0...3).forEach {
result |= UInt32(self.next()!) << UInt32($0 * 8)
}
return result
}
}
extension Thing {
init(inout input:IndexingGenerator<[UInt8]>) {
self.init(a: input._UInt8(), b: input._UInt32(), c: input._UInt8())
}
}
input = output.generate()
let thing3 = Thing(input: &input)
"\(thing3.a), \(thing3.b), \(thing3.c)" // --> "66, 3735928495, 19"
Basically, I move the various stream decoding methods to byte stream (i.e. GeneratorType where Element == UInt8), and then I just have to write an initializer that strings those off in the same order and type the struct is defined as. I guess that part, which is essentially "copying" the structure definition itself (and therefore error prone), is what I had hoped to use some sort of introspection to handle. Mirrors are the only real Swift introspection I'm aware of, and it seems pretty limited.
As discussed in the comments, I suspect this is over-clever. Swift includes a lot of types not friendly to this approach. I would focus instead on how to make the boilerplate as easy as possible, without worrying about eliminating it. For example, this is very sloppy, but is in the direction I would probably go:
Start with some helper packer/unpacker functions:
func pack(values: Any...) -> [UInt8]{
var output:[UInt8] = []
for value in values {
switch value {
case let i as UInt32:
(0...3).forEach { output.append(UInt8((i >> ($0 * 8)) & 0xFF)) }
case let i as UInt8:
output.append(i)
default:
assertionFailure("Don't know how to serialize \(value.dynamicType)")
}
}
return output
}
func unpack<T>(bytes: AnyGenerator<UInt8>, inout target: T) throws {
switch target {
case is UInt32:
var newValue: UInt32 = 0
(0...3).forEach {
newValue |= UInt32(bytes.next()!) << UInt32($0 * 8)
}
target = newValue as! T
case is UInt8:
target = bytes.next()! as! T
default:
// Should throw an error here probably
assertionFailure("Don't know how to deserialize \(target.dynamicType)")
}
}
Then just call them:
struct Thing {
var a:UInt8 = 0
var b:UInt32 = 0
var c:UInt8 = 0
func encode() -> [UInt8] {
return pack(a, b, c)
}
static func decode(bytes: [UInt8]) throws -> Thing {
var thing = Thing()
let g = anyGenerator(bytes.generate())
try unpack(g, target: &thing.a)
try unpack(g, target: &thing.b)
try unpack(g, target: &thing.c)
return thing
}
}
A little more thought might be able to make the decode method a little less repetitive, but this is still probably the way I would go, explicitly listing the fields you want to encode rather than trying to introspect them. As you note, Swift introspection is very limited, and it may be that way for a long time. It's mostly used for debugging and logging, not logic.
I have tagged Rob's answer is the official answer. But I'd thought I'd share what I ended up doing as well, inspired by the comments and answers.
First, I fleshed out my "Problem" a little to include a nested structure:
struct Inner {
var ai:UInt16 = 0
var bi:UInt8 = 0
}
struct Thing {
var a:UInt8 = 0
var b:UInt32 = 0
var inner = Inner()
var c:UInt8 = 0
}
sizeof(Thing) // --> 12 :(
var thing = Thing(a: 0x42, b: 0xDEADBEAF, inner: Inner(ai: 0x1122, bi: 0xDD), c: 0x13)
var data = NSData(bytes: &thing, length: sizeof(Thing)) // --> <42000000 afbeadde 2211dd13> :(
For Arbitrary Packing, I stuck with the same generic approach:
protocol Packable {
func packed() -> [UInt8]
}
extension UInt8:Packable {
func packed() -> [UInt8] {
return [self]
}
}
extension UInt16:Packable {
func packed() -> [UInt8] {
return [(UInt8((self >> 0) & 0xFF)), (UInt8((self >> 8) & 0xFF))]
}
}
extension UInt32:Packable {
func packed() -> [UInt8] {
return [(UInt8((self >> 0) & 0xFF)), (UInt8((self >> 8) & 0xFF)), (UInt8((self >> 16) & 0xFF)), (UInt8((self >> 24) & 0xFF))]
}
}
extension Packable {
func packed() -> [UInt8] {
let mirror = Mirror(reflecting:self)
var bytes:[UInt8] = []
mirror.children.forEach { (label, child) in
switch child {
case let value as Packable:
bytes += value.packed()
default:
print("Don't know how to serialize \(child.dynamicType) (field \(label))")
}
}
return bytes
}
}
Being able to "pack" things is as easy adding them to the Packable protocol and telling them to pack themselves. For my cases above, I only need 3 different types of signed integers, but one could add lots more. For example, in my own code, I have some Enums derived from UInt8 which I added the packed method to.
extension Thing:Packable { }
extension Inner:Packable { }
var output = thing.packed()
output.count // --> 9 :)
data = NSData(bytes: &output, length: output.count) // --> <42afbead de2211dd 13> :)
To be able to unpack stuff, I came up with a little bit of support:
protocol UnpackablePrimitive {
static func unpack(inout input:IndexingGenerator<[UInt8]>) -> Self
}
extension UInt8:UnpackablePrimitive {
static func unpack(inout input:IndexingGenerator<[UInt8]>) -> UInt8 {
return input.next()!
}
}
extension UInt16:UnpackablePrimitive {
static func unpack(inout input:IndexingGenerator<[UInt8]>) -> UInt16 {
return UInt16(input.next()!) | (UInt16(input.next()!) << 8)
}
}
extension UInt32:UnpackablePrimitive {
static func unpack(inout input:IndexingGenerator<[UInt8]>) -> UInt32 {
return UInt32(input.next()!) | (UInt32(input.next()!) << 8) | (UInt32(input.next()!) << 16) | (UInt32(input.next()!) << 24)
}
}
With this, I can then add initializers to my high level structures, e.g.
extension Inner:Unpackable {
init(inout packed bytes:IndexingGenerator<[UInt8]>) {
self.init(ai: UInt16.unpack(&bytes), bi: UInt8.unpack(&bytes))
}
}
extension Thing:Unpackable {
init(inout packed bytes:IndexingGenerator<[UInt8]>) {
self.init(a: UInt8.unpack(&bytes), b: UInt32.unpack(&bytes), inner: Inner(packed:&bytes), c: UInt8.unpack(&bytes))
}
}
What I liked about this is that these initializers call the default initializer in the same order and types as the structure is defined. So if the structure changes in type or order, I have to revisit the (packed:) initializer. The kids a bit long, but not too.
What I didn't like about this, was having to pass the inout everywhere. I'm honestly not sure what the value is of value based generators, since passing them around you almost always want to share state. Kind of the whole point of reifying an object that captures the position of a stream of data, is to be able to share it. I also don't like having to specify IndexingGenerator directly, but I imagine there's some fu magic that would make that less specific and still work, but I'm not there yet.
I did play with something more pythonic, where I return a tuple of the type and the remainder of a passed array (rather than a stream/generator), but that wasn't nearly as easy to use at the top level init level.
I also tried putting the static methods as extensions on byte based generators, but you have to use a function (would rather have used a computed var with side effects) there whose name doesn't match a type, so you end up with something like
self.init(a: bytes._UInt8(), b: bytes._UInt32(), inner: Inner(packed:&bytes), c: bytes._UInt8())
This is shorter, but doesn't put the type like functions next to the argument names. And would require all kinds of application specific method names to be added as well as one extended the set of UnpackablePrimitives.