(Mac) Wi-Fiが切れたら自動で再接続するメニューバー常駐アプリケーション

## 背景
うちの賃貸マンションはNuro光が無料で使えるのですが、手持ちのMacと相性が悪いのか、突然接続が切れることが度々あります。1日に1回以上の頻度で発生するため、さすがに対策が必要でした。

単にこちらをShell scriptあるいはApple Scriptで実装してもよいのですが、稼働している様子がわかるようにしたく、メニューバーに常駐するタイプのアプリケーションとして実装することにしました。

https://superuser.com/questions/335117/how-to-get-a-mac-to-auto-reconnect-to-a-wifi-network/346355


## 方法

基本的には、こちらのチュートリアルの通りに進めてメニューバー常駐アプリを作りました。

https://clrmemory.com/programming/swift/cocoa-menubar-application/


Swiftからshellコマンドを実行する方法としてこちらを使いました。

https://gist.github.com/andreacipriani/8c3af3719da31c8fae2cdfa8c21e17ba


## 完成コード


```swift:AppDelegate.swift
import Cocoa


protocol CommandExecuting {
    func run(commandName: String, arguments: [String]) throws -> String
}

enum BashError: Error {
    case commandNotFound(name: String)
}


struct Bash: CommandExecuting {
    func run(commandName: String, arguments: [String] = []) throws -> String {
        return try run(resolve(commandName), with: arguments)
    }
    
    private func resolve(_ command: String) throws -> String {
        guard var bashCommand = try? run("/bin/bash" , with: ["-l", "-c", "which \(command)"]) else {
            throw BashError.commandNotFound(name: command)
        }
        bashCommand = bashCommand.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines)
        return bashCommand
    }
    
    private func run(_ command: String, with arguments: [String] = []) throws -> String {
        let process = Process()
        process.launchPath = command
        process.arguments = arguments
        let outputPipe = Pipe()
        process.standardOutput = outputPipe
        process.launch()
        let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(decoding: outputData, as: UTF8.self)
        return output
    }
}


@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    
    
    @IBOutlet weak var menu: NSMenu!
    
    //メニューバーに表示されるアプリケーションを作成
    let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.variableLength)
    
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // メニューバーに表示されるアプリ。今回は文字列で設定
        statusItem.title = "NURO"
        statusItem.highlightMode = true
        statusItem.menu = menu
        
        
        //アプリ終了ボタンを作成
        let quitItem = NSMenuItem()
        quitItem.title = "Quit Application"
        quitItem.action = #selector(AppDelegate.quit(_:))
        menu.addItem(quitItem)
        
        // WiFi再接続のループ開始
        startLoop()
    }
    
    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }
    
    @objc func quit(_ sender: Any) {
        NSApplication.shared.terminate(self)
    }
    
    
    func startLoop() {
        Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block:{ (timer) in
            
            let bash: CommandExecuting = Bash()
            var result = ""
            do {
                try result = bash.run(commandName: "networksetup", arguments: ["-getinfo", "Wi-Fi"])
            }catch {
                print("failed to networksetup -getinfo")
                return
            }
            
            
            if (!result.contains("Subnet mask")) {
                do {
                    try result = bash.run(commandName: "networksetup", arguments:
                        ["-setairportnetwork", "en1", "HG8045-D94E-bg", "cuayh5ks"]
                    )
                } catch {
                    print("failed to networksetup -setairportnetwork")
                }
            }
            
            
        })
        
    }
    
}
```

## ポイント

.entitlementsファイルで、`App Sandbox`を`NO`にする必要がある。


## 動作の様子

![スクリーンショット 2021-05-05 20.35.25.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/801279/12f8d22d-c56e-1457-e2e7-498ca98db498.jpeg)