Scheduling IOHIDManager with runloop in a GUI App - swift

I am writing an app that is monitoring input from a gamepad in swift.
I managed to build a command-line application with the intended behavior :
import Foundation
import IOKit.hid
var valueCallback : IOHIDValueCallback = {
(context, result, sender, value) in
let element = IOHIDValueGetElement(value)
print(IOHIDElementGetUsage(element), IOHIDValueGetIntegerValue(value))
}
var attachCallback : IOHIDDeviceCallback = {
(context, result, sender, device) in
IOHIDDeviceOpen(device, IOOptionBits(kIOHIDOptionsTypeSeizeDevice))
IOHIDDeviceRegisterInputValueCallback(device, valueCallback, context)
print("Controller attached")
}
var detachCallback : IOHIDDeviceCallback = {
(context, result, sender, device) in
print("Controller detached")
}
class HID {
let manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone))
let devices = [kIOHIDTransportKey: "Bluetooth"] as CFDictionary
init() {
IOHIDManagerSetDeviceMatching(self.manager, self.devices)
IOHIDManagerRegisterDeviceMatchingCallback(self.manager, attachCallback, nil)
IOHIDManagerRegisterDeviceRemovalCallback(self.manager, detachCallback, nil)
IOHIDManagerScheduleWithRunLoop(self.manager, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue)
IOHIDManagerOpen(self.manager, IOOptionBits(kIOHIDOptionsTypeNone))
}
}
var hidTest = HID()
CFRunLoopRun()
I then want to use this inside an actual app, but it then doesn't work as intended. I used the same code for the class and callbacks, and tried this in the AppDelegate :
import Cocoa
import SwiftUI
import AppKit
import IOKit.hid
import Foundation
#main
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView()
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
let hidTest = HID()
CFRunLoopRun()
}
}
Everything compiles fine, but I am not able to get any value from the gamepad. I think the problem is there : IOHIDManagerScheduleWithRunLoop(self.manager, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) because the manager is probably not attached to the app's RunLoop.
Is there something specific I need to do to make this work ?
Thanks in advance !

I'm not a Swift expert, but I do know about IOKit and runloops, so I'll attempt an answer.
I think it's likely there is a problem with the call to CFRunLoopRun(). Cocoa apps already run in a runloop which is implicitly created and run by NSApplication. Don't explicitly run the runloop, just allow applicationDidFinishLaunching to return.
When you do this, you should probably make your HID object a member of your application object or similar, or it'll be destroyed. I'm not sure if the Swift bindings for IOKit perform any automatic resource destruction; if so, this would presumably destroy the HID manager object, which you probably want to stay alive for the duration of you application's lifetime.

Related

Cannot inject AppDelegate to SwiftUI macos app

I'm new to swift development and stuck developing sattus bar application. Found some good toturials (first, second, third, fourth), and in eash of them at some point authors inject AppDelegate to : App class, kinda:
import SwiftUI
#main
struct CaptureOneAppApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
And in tutorials similar code works just fine, but me getting compile-time error pointing row #5:
Type annotation missing in pattern
As I understand, compiler asks me to add some suffix with type definition, but i have no idea how it should look like and actually not sure if this is a correct way to solve the problem. So, what I'm doing wrong?
UPD AppDelegate.swift code:
import Cocoa
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var statusBar: StatusBarController?
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
//Initialising the status bar
statusBar = StatusBarController.init()
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
it's just copypasted from first

Xcode SwiftUI window not shown after successful build

I get the following warning, on main.storyboard
Unsupported Configuartion:
Window Controller requires a content view controller
This is the custom class linked to the Window Controller, which is also the StoryBoard Entry Point,
import Cocoa
import SwiftUI
class FirstWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
let contentView = ContentView(myListModel: MyListModel())
.environmentObject(UserData())
self.window?.contentView = NSHostingView(rootView: contentView)
}
}
This is inside the AppDelegate.swift which is annotated as #NSApplicationMain.
func applicationDidFinishLaunching(_ aNotification: Notification) {
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window”)
}
The main.storyboard, AppDelegate.swift, and FirstWindowController.swift are identical to those of a project which was launching and showing the application window with no problem. I just renamed it to something else, and removed the Core Data support from the code. But in this project, the window doesn’t show up after successful build.
I also have checked and compared all the configuration of the storyboard for both of these projects. They seem to be totally identical.
Any help for fixing this would be appreciated.
This might be due to a bug.
I removed all the other schemes but the one I was using in the project and the window is now loading.
Yet, the same warning is still there.

Draw shape around cursor on macOS using Swift [duplicate]

This question already has an answer here:
How do I change my cursor on the whole screen? (not only current view/window)
(1 answer)
Closed 1 year ago.
I'm pretty new to macOS and swift development and I have been watching a few tutorials to figure this out. I also watched a udemy course that worked on 18 macOS projects which got me nowhere
All I want to do is a macOS menu bar app that will add a cursor highlight that should look something like:
I could get the cursor changed to an image doing the following
import SpriteKit
class CursorView: SKView {
override func resetCursorRects() {
if let targetImage = NSImage(named: "cursor") {
let cursor = NSCursor(image: targetImage,
hotSpot: CGPoint(x: targetImage.size.width / 2,
y: targetImage.size.height / 2))
addCursorRect(frame, cursor: cursor)
}
}
}
Three things wrong with this:
SKView is a class from SpriteKit and I don't think I should use that for my use-case
This calls the addCursorRect that add the changes to a window frame (I need to all the time regardless of the frame)
I can't have 100's of images for each style I would set in the future for the highlight color, size, or opacity
So, I'm here trying to understand how I can do this for a menu bar app that should be available on all screens and achieve a highlight as should in the above picture
Not sure if this matters but I'm using storyboard and don't mind switching to SwiftUI
I could really use some help from the community on this. Thank you
You can annotate the system mouse by drawing something around it. This can be done by
adding an CGEvent tap to capture mouse events
drawing the annotation around the cursor using a custom window
Here is a simple example application
import SwiftUI
#main
class AppDelegate: NSObject, NSApplicationDelegate {
var mouseTap: CFMachPort?
private var window: NSWindow = {
// Create the SwiftUI view that provides the window contents.
let contentView = Circle()
.stroke(lineWidth: 2)
.foregroundColor(.blue)
.frame(width: 30, height: 30)
.padding(2)
// Create the window and set the content view.
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 34, height: 34),
styleMask: [.borderless],
backing: .buffered,
defer: false
)
window.contentView = NSHostingView(rootView: contentView)
window.backgroundColor = .clear
window.level = NSWindow.Level.statusBar
window.makeKeyAndOrderFront(nil)
return window
}()
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let tap = createMouseTap() {
if CGEvent.tapIsEnabled(tap: tap) {
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, CFRunLoopMode.commonModes)
mouseTap = tap
} else {
print("tap not enabled")
mouseTap = nil
}
} else {
print("tap not enabled")
}
}
func createMouseTap() -> CFMachPort? {
withUnsafeMutableBytes(of: &window) { pointer in
CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: CGEventTapOptions.listenOnly,
eventsOfInterest: (1 << CGEventType.mouseMoved.rawValue | 1 << CGEventType.leftMouseDragged.rawValue),
callback: mouseMoved,
userInfo: pointer.baseAddress
)
}
}
}
func mouseMoved(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, context: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
let window = context!.assumingMemoryBound(to: NSWindow.self).pointee
// using CGPoint+SIMD extension from https://gist.github.com/Dev1an/7973cee9d960479b35b705f88b7f38c4
window.setFrameOrigin(event.unflippedLocation - 17)
return nil
}
Drawback
Note that this implementation does require the user to allow the keyboard input option in "Universal Access" in "System Preferences".

NSWorkspace didMountNotification / didUnmountNotification not triggered

I am in the process of creating a new macOS application that is going to perform some action whenever a USB device (or other storage device) has been attached to the computer. It also needs to run silently in the background.
For the USB part, I have added the following class to the project:
import Foundation
import AppKit
class UsbDetector {
init() {
print("Hello from UsbDetector")
let notificationCenter = NSWorkspace.shared.notificationCenter
notificationCenter.addObserver(self, selector: #selector(didMount(_:)), name: NSWorkspace.didMountNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(didUnmount(_:)), name: NSWorkspace.didUnmountNotification, object: nil)
}
#objc func didMount(_ notification: NSNotification) {
print("Hello from mounted")
if let devicePath = notification.userInfo!["NSDevicePath"] as? String {
print("Mounted: \(devicePath)")
}
}
#objc func didUnmount(_ notification: NSNotification) {
print("Hello from unmounted")
if let devicePath = notification.userInfo!["NSDevicePath"] as? String {
print("Unmounted: \(devicePath)")
}
}
}
I instantiate the UsbDetector from AppDelegate.swift as follows:
import Cocoa
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
print("Hello from AppDelegate")
// Start UsbDetector
_ = UsbDetector()
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
To make it run in the background, I have added the following key to the Info.plist file of the project:
<key>LSBackgroundOnly</key>
<true/>
When the app is started, it is visible in the Activity Monitor but nowhere else, which is fine. But when I attach / detach a USB stick, the NSWorkspace event is not triggered. All that gets printed to the console is:
Hello from AppDelegate
Hello from UsbDetector
I am using Xcode 11.5 and macOS Catalina 10.15.5.
Can someone tell me what I am doing wrong here? Thanks in advance!

Display window on OSX using Swift without XCode or NIB

Disclaimer: I'm attempting the following exercise because I think it will be instructive. I'm interested in how it might be done. So please don't be too hasty to jump in with "This is the wrong way to do it, you should never do it like this!"
Working from the commandline with my favourite text editor, I would like to construct a minimal Swift program that displays a window.
It's a GUI/Cococa hello world, if you like.
In the same spirit, I want to avoid NIB.
So, No XCode, No NIB.
I would like to:
compile it with the swift compiler
create a #! swift script which runs using the Swift interpreter
If I can do both of those things I will feel my feet on the ground and be much more at ease upgrading to Xcode.
I tried the following:
window.swift
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
let newWindow = NSWindow(contentRect : NSScreen.mainScreen()!.frame
, styleMask : NSBorderlessWindowMask
, backing : NSBackingStoreType.Buffered
, defer : false)
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
newWindow.opaque = false
newWindow.movableByWindowBackground = true
newWindow.backgroundColor = NSColor.whiteColor()
newWindow.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
}
However, attempting to run this from the command line fails:
pi#piBookAir.local ~ /Users/pi/dev/macdev:
⤐ swift window.swift
window.swift:3:1: error: 'NSApplicationMain' attribute cannot be used in a
module that contains top-level code
#NSApplicationMain
^
window.swift:1:1: note: top-level code defined in this source file
import Cocoa
^
✘
What's the correct way to eliminate the error?
Porting this code from objective-c to Swift, you get
import Cocoa
let nsapp = NSApplication.shared()
NSApp.setActivationPolicy(NSApplicationActivationPolicy.regular)
let menubar = NSMenu()
let appMenuItem = NSMenuItem()
menubar.addItem(appMenuItem)
NSApp.mainMenu = menubar
let appMenu = NSMenu()
let appName = ProcessInfo.processInfo.processName
let quitTitle = "Quit " + appName
let quitMenuItem = NSMenuItem.init(title:quitTitle,
action:#selector(NSApplication.terminate),keyEquivalent:"q")
appMenu.addItem(quitMenuItem);
appMenuItem.submenu = appMenu;
let window = NSWindow.init(contentRect:NSMakeRect(0, 0, 200, 200),
styleMask:NSTitledWindowMask,backing:NSBackingStoreType.buffered,defer:false)
window.cascadeTopLeft(from:NSMakePoint(20,20))
window.title = appName;
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps:true)
NSApp.run()
Save it as minimal.swift, compile
swiftc minimal.swift -o minimal
and run
./minimal
You will get an empty window and a menu bar with a menu named like the application and a quit button.
Why it works exactly, I don't know. I'm new to Swift and Cocoa programming, but the linked website explains a bit.
Make a file TestView.swift (like this):
import AppKit
class TestView: NSView
{
override init(frame: NSRect)
{
super.init(frame: frame)
}
required init?(coder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
var colorgreen = NSColor.greenColor()
override func drawRect(rect: NSRect)
{
colorgreen.setFill()
NSRectFill(self.bounds)
let h = rect.height
let w = rect.width
let color:NSColor = NSColor.yellowColor()
let drect = NSRect(x: (w * 0.25),y: (h * 0.25),width: (w * 0.5),height: (h * 0.5))
let bpath:NSBezierPath = NSBezierPath(rect: drect)
color.set()
bpath.stroke()
NSLog("drawRect has updated the view")
}
}
Make a file TestApplicationController.swift (like this):
import AppKit
final class TestApplicationController: NSObject, NSApplicationDelegate
{
/// Seems fine to create AppKit UI classes before `NSApplication` object
/// to be created starting OSX 10.10. (it was an error in OSX 10.9)
let window1 = NSWindow()
let view1 = TestView(frame: NSRect(x: 0, y: 0, width: 1000, height: 1000))
func applicationDidFinishLaunching(aNotification: NSNotification)
{
window1.setFrame(CGRect(x: 0, y: 0, width: 1000, height: 1000), display: true)
window1.contentView = view1
window1.opaque = false
window1.center();
window1.makeKeyAndOrderFront(self)
// window1.backgroundColor = view1.colorgreen
// window1.displayIfNeeded()
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
}
Make a file main.swift (like this):
//
// main.swift
// CollectionView
//
// Created by Hoon H. on 2015/01/18.
// Copyright (c) 2015 Eonil. All rights reserved.
//
import AppKit
let app1 = NSApplication.sharedApplication()
let con1 = TestApplicationController()
app1.delegate = con1
app1.run()
The last file must not be renamed, main.swift is apparently a special name for swift (otherwise the example will not compile).
Now, enter this (to compile the example):
swiftc -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk TestView.swift TestApplicationController.swift main.swift
Run the code by entering:
./main
It shows a main window (centered) with a nice green collor and a yellow rectangle within it).
You can kill the app by entering Control-C.
Note that this is a swift compilation not an interpreter running, so you have a native app.
Note that -sdk and the path to MacOSX10.11.sdk is essential (otherwise the code will not compile).
Note also that this compilation depends on the latest Xcode distribution, so update MacOSX10.11.sdk to MacOSX10.10.sdk or whatever is in the SDKs directory.
It took a while to find this out ...
Based on the other solutions I wrote a recent version of a single file app.
Requirements: Swift 5.6, Command Line Tools, no XCode, no XIB, no Storyboard.
One file, one class, run it with swift app.swift.
file: app.swift
import AppKit
class App : NSObject, NSApplicationDelegate {
let app = NSApplication.shared
let name = ProcessInfo.processInfo.processName
let status = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
let window = NSWindow.init(
contentRect: NSMakeRect(0, 0, 200, 200),
styleMask: [.titled, .closable, .miniaturizable],
backing: .buffered,
defer: false
)
override init() {
super.init()
app.setActivationPolicy(.accessory)
window.center()
window.title = name
window.hidesOnDeactivate = false
window.isReleasedWhenClosed = false
let statusMenu = newMenu()
status.button?.title = "🤓"
status.menu = statusMenu
let appMenu = newMenu()
let sub = NSMenuItem()
sub.submenu = appMenu
app.mainMenu = NSMenu()
app.mainMenu?.addItem(sub)
}
#IBAction func activate(_ sender:Any?) {
app.setActivationPolicy(.regular)
DispatchQueue.main.async { self.window.orderFrontRegardless() }
}
#IBAction func deactivate(_ sender:Any?) {
app.setActivationPolicy(.accessory)
DispatchQueue.main.async { self.window.orderOut(self) }
}
private func newMenu(title: String = "Menu") -> NSMenu {
let menu = NSMenu(title: title)
let q = NSMenuItem.init(title: "Quit", action: #selector(app.terminate(_:)), keyEquivalent: "q")
let w = NSMenuItem.init(title: "Close", action: #selector(deactivate(_:)), keyEquivalent: "w")
let o = NSMenuItem.init(title: "Open", action: #selector(activate(_:)), keyEquivalent: "o")
for item in [o,w,q] { menu.addItem(item) }
return menu
}
func applicationDidFinishLaunching(_ n: Notification) { }
func applicationDidHide(_ n: Notification) {
app.setActivationPolicy(.accessory)
DispatchQueue.main.async { self.window.orderOut(self) }
}
}
let app = NSApplication.shared
let delegate = App()
app.delegate = delegate
app.run()
The 64 lines of code above provide the following features and fixes over the previous solutions:
app menu is visible and usable
status menu can be clicked and is usable
both menus have working key keybindings
dock item appears when window is open:
app.setActivationPolicy(.regular)
dock item hides when window is closed:
app.setActivationPolicy(.accessory)
window is preserved on close and reused on open:
window.hidesOnDeactivate = false
window.isReleasedWhenClosed = false
Tested on M1 Pro. Here is how it looks.
Pass the -parse-as-library flag to swiftc:
swiftc -parse-as-library window.swift
That said, you'll just end up running into a second error when you do this:
2015-06-10 14:17:47.093 window[11700:10854517] No Info.plist file in application bundle or no NSPrincipalClass in the Info.plist file, exiting