BetterSnapToolやMagnetのようなmacOS用のウィンドウ配置マネージャーツールを再現してみました。現在の最前面のウィンドウを右半分に配置したり、左3分の1に配置したり、最大限広げて配置したりといったことがメニューバーのコマンドやショートカットキーをトリガーとして行えます。
動機
そもそも普通のアプリというのはSandBoxに守られているため、外部のアプリの管轄であるウィンドウを制御することは本来できません。常駐型アプリであること、別アプリのウィンドウを制御していることに興味があったためどのような技術でこれが実現できているのか調査したいと思いました。
技術に関して
アプリを常駐させる
常駐型アプリを作る方法については別途記事があるのでこちらをご参考に。
https://zenn.dev/kyome/articles/02d9f969fd17e5
ウィンドウを制御する
まず、最前面にあるウィンドウのアプリを取得するには、NSWorkspace
の API を用いれば良いです。
let app = NSWorkspace.shared.runningApplications.first(where: { $0.isActive })
あとは、ApplicationServices
を介してアクセシビリティの API を使いウィンドウを制御しました。AXUIElement
というオブジェクトでウィンドウの情報を取得したり上書きすることが可能です。上で取得したアプリのpid
を用いるとAXUIElement
を生成できます。
AXUIElementCopyAttributeValue
という API を用いると指定したAUIElement
の情報を取得することができます。
// 頻繁に使うため便利関数化
func copyAttributeValue(_ element: AXUIElement, attribute: String) -> CFTypeRef? {
var ref: CFTypeRef? = nil
let error = AXUIElementCopyAttributeValue(element, attribute as CFString, &ref)
if error == .success {
return ref
}
return .none
}
// 情報を取得する例
// ウィンドウの役割をStringで取得
func getRole(element: AXUIElement) -> String? {
return self.copyAttributeValue(element, attribute: kAXRoleAttribute) as? String
}
// ウィンドウがフルスクリーンかどうかをBoolで取得
func isFullscreen(element: AXUIElement) -> Bool {
let result = self.copyAttributeValue(element, attribute: "AXFullScreen") as? NSNumber
return result?.boolValue ?? false
}
attribute にkAX
から始まるキーを指定してCFTypeRef = AnyObject
型の情報を取得して、のちに適切な型に変換します。ウィンドウの座標や大きさなどの情報をCGPoint
やCGSize
で取得する場合は、さらにAXValueGetValue
というAPIを重ねて使用する必要があります。GtiHubでコード検索するとやり方はわかります。
AUIElement
の情報を上書きする場合は、AXUIElementSetAttributeValue
というAPIを用います。
// 頻繁に使うため便利関数化
func setAttributeValue(_ element: AXUIElement, attribute: String, value: CFTypeRef) -> Bool {
let error = AXUIElementSetAttributeValue(element, attribute as CFString, value)
return error == .success
}
// 情報を上書きする例
// 座標を更新
@discardableResult
func setPosition(element: AXUIElement, position: CGPoint) -> Bool {
var position = position
if let value = AXValueCreate(AXValueType.cgPoint, &position) {
return self.setAttributeValue(element, attribute: kAXPositionAttribute, value: value)
}
return false
}
更新したい値をAXValue
の型に変換してから、kAX
から始まるキーと一緒に渡せば更新できます。ウィンドウの制御の根幹はこんな感じです。
制限事項
- 現在は、アクセシビリティの API をApp SandBox環境では叩けないようになっているため。SandBoxを外す必要があります。必然的に App Store で配信することは不可能です。BetterSnapToolやMagnetは規制が厳しくなかった頃からリリースされていたから特別に許されているという感じでしょうか。ただ、そもそもバイナリ提出で却下される気がするので、特別な App Sandbox Temporary Exception Entitlements を指定しているかもしれません。
- アクセシビリティの API を叩くには専用の権限をユーザーに付与してもらう必要があります。システム環境設定 -> セキュリティとプライバシー -> アクセシビリティの項目でアプリごとに許可を切り替えられます。故に現在権限が付与されているか確認し、ユーザに催促する仕組みが必要です。
配布
GitHubにソースもアプリも公開してあります。
もっとカスタマイズしたい方のコミットをお待ちしています。
https://github.com/Kyome22/ShiftWindow