まずはこちらをご覧ください。
URLをコピーすると自動でQRコードに自動変換して表示してくれてますね。動作としてはURLと思われるテキストをコピーした後、Cmd + Ctrl + C
のショートカットでQRコードを表示するようにしています。つまり Cmd + C
→ Cmd + Ctrl + C
のキーボード操作だけでいけます。便利ですね。
名前は LightningQR⚡️ です。ライトニングさんは関係ありません。
今回はこのMacのステータスバーに常駐するアプリをSwift3で作ったので、簡単に作り方を解説しようと思います。似たようなアプリを作ろうと思っている誰かのお役に立てれば幸いです。
なお実際に使ってみたい人は、下記からダウンロードしてください😀 またソースコードもGitHubで公開しているので見てみてください。Macアプリ初心者が作ったのでハッキリ言って雑コードです。ごめんなさい🙇
なぜ作ったのか
僕はスマホ用のWebサービスの開発中、実機で動作確認をしたいときはURLはQRコードに変換して、スマホのQRコードリーダーで読み取っていました。普段使いしているiPhoneとかであれば、もしかしたらもっとスマートに共有する方法があるのかもしれませんが、手元にある検証機は特に何も入ってない・アカウントの連携もされていない無味乾燥なやつなのでこうしてます。
これはスマホ側は楽でいいんですが、MacでいちいちURLをQRコードに変換するのが面倒くさかったんです。数十秒くらいでできるとはいえ、けっこうなストレスです。なのでキーボード操作だけで済むようにしたかったのが作った理由です。
常駐アプリとして起動する
クリップボードを監視したいので普通のアプリとは違い常駐アプリとして起動します。設定はとても簡単です。
TARGETS→アプリ名→Infoタブを開いて、Custom macOS Application Target Propertiesの欄に新しく「Application is agent (UIElement)」というキーを追加します。値は「YES」です。これだけです。
この設定をするとアプリを起動してもDockには表示されません。なのでアプリを終了させるには終了メニューを用意していないとkillなどでプロセスを殺すしかなくなります。
ステータスバーにアイコンを表示する
次にMacのステータスバーにアイコンを表示させます。ちなみに上にあるバー自体はメニューバーという名前で、右側にアプリアイコンとか時計が表示されているエリアをステータスバーと呼ぶようです。参考
あらかじめ StatusImage
という名前のImageSetを作っておきます。サイズは 16px と 32px の2種類を用意しました。16pxのアイコンが非常に小さくて作るのが大変でした…。常に表示されるものなので色はモノクロにしました。
AppDelegateにてアイコンを登録します。
class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem = NSStatusBar.system().statusItem(withLength: NSSquareStatusItemLength)
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let button = statusItem.button {
button.image = NSImage(named: "StatusImage")
button.action = #selector(hogeAction)
}
}
}
今回はアイコンだけを表示するので NSSquareStatusItemLength
を指定しています。文字など他の情報を表示するような場合は NSVariableStatusItemLength
を指定してください。
これでステータスバーにアイコンが表示されていると思います。
ポップオーバーのViewを表示する
QRコードを表示するためのViewを用意します。せっかくステータスバーにアイコンがあるのでそこからニョキッとでるようなViewが良いですね。 NSPopover
というAPIでそれができるみたいなのでこれを使います。
class AppDelegate: NSObject, NSApplicationDelegate {
let popover = NSPopover()
func applicationDidFinishLaunching(_ aNotification: Notification) {
popover.behavior = .transient
popover.contentViewController = QrcodeViewController(nibName: "QrcodeViewController", bundle: nil)
}
func togglePopover(sender: AnyObject?) {
if popover.isShown {
closePopover(sender: sender)
} else {
showPopover(sender: sender)
}
}
func showPopover(sender: AnyObject?) {
if let button = statusItem.button {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
NSApplication.shared().activate(ignoringOtherApps: true)
}
}
func closePopover(sender: AnyObject?) {
popover.performClose(sender)
}
}
popover.behavior = .transient
を指定することでNSPopoverのViewからフォーカスが外れた際に自動で閉じてくれます。これがないとずっと開いたままでうざったいです。
表示するViewは popover.contentViewController
にいれてあげればOKです。今回はxibを使用しています。表示する際のアニメーションは show
メソッドの引数で指定しています。ステータスバー常駐の場合は基本 preferredEdge: NSRectEdge.minY
で下に伸びるようにするのが良いと思います。
また NSApplication.shared().activate(ignoringOtherApps: true)
で全てのアプリの上に優先して表示するようにしてくれます。これで一応アプリとしての見た目ができてきました。
クリップボードを監視する
コピーしたタイミングでURL判定をしておきたいのでクリップボードを監視します。 NSPasteboard
というAPIででクリップボードのデータを見ることができます。ただイベントハンドラ的なものはなくタイマーで監視する必要があります。
var changeCount: Int = 0
func beginObservingPasteboard() {
self.changeCount = NSPasteboard.general().changeCount
Timer.scheduledTimer(timeInterval: 1.0,
target: self,
selector: #selector(observePasteboard),
userInfo: nil,
repeats: true)
}
func observePasteboard() {
let pboard = NSPasteboard.general()
if (pboard.changeCount > self.changeCount) {
pasteboardChanged(pboard: pboard)
}
self.changeCount = pboard.changeCount
}
func pasteboardChanged(pboard:NSPasteboard) {
let url = pboard.string(forType: NSStringPboardType)!
}
NSPasteboard.general().changeCount
でコピーするたびにインクリメントされるので、1秒毎に値が変わっていないかをチェックして変わっていれば新しい何かがコピーされたと判断をしています。
コードはこちらの記事を参考にしました。ありがとうございます。
URLを判定する
URL判定は自前正規表現で荒くやろうかなと思ったんですが調べるともっと簡単な方法があったので、そちらを使っています。
class AppDelegate: NSObject, NSApplicationDelegate, Validator {
}
protocol Validator {
func validateURL(urlString: String) -> Bool
}
extension Validator {
func validateURL(urlString: String) -> Bool {
var result = false
let types: NSTextCheckingResult.CheckingType = .link
let detector = try? NSDataDetector(types: types.rawValue)
guard let detect = detector else {
return result
}
let matches = detect.matches(in: urlString, options: .reportCompletion, range: NSMakeRange(0, urlString.characters.count))
for match in matches {
if let url = match.url {
result = true
}
}
return result
}
}
コードはこちらの記事を参考にしました。ありがとうございます。
QRコードを生成する
URLからQRコードを生成する部分ですが、CoreImageのCIFilterの中に CIQRCodeGenerator
というまさになフィルタがあって非常に簡単でした。CIFiler→CIImage→NSImageに変換をしています。
import CoreImage
class Qrcode {
func generateQR (url:String) -> NSImage {
let data = url.data(using: String.Encoding.utf8)
let qr = CIFilter(name: "CIQRCodeGenerator",
withInputParameters: [
"inputMessage": data,
"inputCorrectionLevel": "M"])!
let sizeTransform = CGAffineTransform(scaleX: 10, y: 10)
let qrImage = qr.outputImage!.applying(sizeTransform)
let rep = NSCIImageRep.init(ciImage: qrImage)
let nsimage = NSImage.init(size: rep.size)
nsimage.addRepresentation(rep)
return nsimage
}
}
inputCorrectionLevel
は誤り訂正レベルです。画面が物理的に壊れたりしない限りは特に気にしなくて良いと思います。これでURLをQRコードにすることができました。
なお、CIFilterの一覧はこちら。
ホットキーを登録する
QRコードは生成できましたが、URLをコピーするたびに都度QRコードを表示してたのでは少々うざったいです。95%くらいは普通にMacの中だけで使うようなコピーでしょうし。そこでQRコードの表示をホットキーに任せます。グローバルショートカットとも言うのかな。アプリがアクティブになっていなくても指定キーバインドのイベントハンドラを取ることができるというものです。
これを実現するには EventHotKeyID
や InstallEventHandler
RegisterEventHotKey
などを使えばできるみたいですが、Magnet というナイスなライブラリがあったのでこれを利用します。
Podfileに下記を書いて pod install
します。
pod 'Magnet'
使い方も簡単で、以下のコードだけで Cmd + Ctrl + C
のホットキーを登録しています。各キーの数値についてはこの記事を参考にしました。
import Magnet
if let keyCombo = KeyCombo(keyCode: 8, carbonModifiers: 4352) {
let hotKey = HotKey(identifier: "CommandControlC", keyCombo: keyCombo, target: self, action: #selector(hoge))
hotKey.register()
}
上記の処理を組み合わせれば最低限のアプリとしての挙動をすると思います😀
ログイン項目に登録する
最後にこのアプリをMac起動時に自動起動するようにします。調べてみると主に3つの方法があるようです。
- Launch Services
- Helper Application
- AppleScript
AppStoreで配布するならSandbox環境にする必要があるので必然的にHelper Applicationになります。今回はAppStore以外で配布する予定で、かつSandboxにする予定もないので簡単なLaunch Services形式にしました。
ただこちらもLoginServiceKitという便利なライブラリがあるので、それを利用します。
pod 'LoginServiceKit', :git => 'https://github.com/Clipy/LoginServiceKit.git'
import LoginServiceKit
func addingToLoginItems() {
let appPath = Bundle.main.bundlePath
LoginServiceKit.addLoginItems(at: appPath)
}
これだけでログイン項目にアプリ名が並んで自動的に起動してくれます。ほんとうに便利ですね。
さいごに
今回Macアプリを作ったのは2回目なんですが、ほとんど迷わず作ることができました。というのも、コアで面倒くさい部分は全部ライブラリがやってくれたからです。iOS/Mac開発もRubyGems並にライブラリが主日していて初心者でもなんとか形になるものまで持っていくことがやりやすいなーと感じました。今回のアプリが完成するまで実質3〜4時間ほどしかかかってないです。ライブラリ開発者の皆さんありがとうございます😆
また、分からないことがあっても検索すれば大抵あります。iOSに比べるとMacアプリは情報量は少ないですが英語で検索すれば見つかります。なのでMacアプリに関しては最初から英語クエリで検索したほうが良さそうです。
今後も開発をより加速化するようなMacアプリを作っていきたいと思いました。以上です。ありがとうございました。