39
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

URLをコピーしたらQRコードに自動変換して表示するMacアプリをSwiftで作った

Posted at

まずはこちらをご覧ください。

87444e922bb80f77905d005f4879bf95.gif

URLをコピーすると自動でQRコードに自動変換して表示してくれてますね。動作としてはURLと思われるテキストをコピーした後、Cmd + Ctrl + CのショートカットでQRコードを表示するようにしています。つまり Cmd + CCmd + Ctrl + C のキーボード操作だけでいけます。便利ですね。

名前は LightningQR⚡️ です。ライトニングさんは関係ありません。

LightningQR-128.png

今回はこのMacのステータスバーに常駐するアプリをSwift3で作ったので、簡単に作り方を解説しようと思います。似たようなアプリを作ろうと思っている誰かのお役に立てれば幸いです。

なお実際に使ってみたい人は、下記からダウンロードしてください😀 またソースコードもGitHubで公開しているので見てみてください。Macアプリ初心者が作ったのでハッキリ言って雑コードです。ごめんなさい🙇

なぜ作ったのか

僕はスマホ用のWebサービスの開発中、実機で動作確認をしたいときはURLはQRコードに変換して、スマホのQRコードリーダーで読み取っていました。普段使いしているiPhoneとかであれば、もしかしたらもっとスマートに共有する方法があるのかもしれませんが、手元にある検証機は特に何も入ってない・アカウントの連携もされていない無味乾燥なやつなのでこうしてます。

これはスマホ側は楽でいいんですが、MacでいちいちURLをQRコードに変換するのが面倒くさかったんです。数十秒くらいでできるとはいえ、けっこうなストレスです。なのでキーボード操作だけで済むようにしたかったのが作った理由です。

常駐アプリとして起動する

クリップボードを監視したいので普通のアプリとは違い常駐アプリとして起動します。設定はとても簡単です。

1.png

TARGETS→アプリ名→Infoタブを開いて、Custom macOS Application Target Propertiesの欄に新しく「Application is agent (UIElement)」というキーを追加します。値は「YES」です。これだけです。

この設定をするとアプリを起動してもDockには表示されません。なのでアプリを終了させるには終了メニューを用意していないとkillなどでプロセスを殺すしかなくなります。

ステータスバーにアイコンを表示する

次にMacのステータスバーにアイコンを表示させます。ちなみに上にあるバー自体はメニューバーという名前で、右側にアプリアイコンとか時計が表示されているエリアをステータスバーと呼ぶようです。参考

あらかじめ StatusImage という名前のImageSetを作っておきます。サイズは 16px と 32px の2種類を用意しました。16pxのアイコンが非常に小さくて作るのが大変でした…。常に表示されるものなので色はモノクロにしました。

2.png

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 を指定してください。

これでステータスバーにアイコンが表示されていると思います。

3.png

ポップオーバーの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コードの表示をホットキーに任せます。グローバルショートカットとも言うのかな。アプリがアクティブになっていなくても指定キーバインドのイベントハンドラを取ることができるというものです。

これを実現するには EventHotKeyIDInstallEventHandler 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アプリを作っていきたいと思いました。以上です。ありがとうございました。

39
30
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?