macOSのスクリーンセーバーをHTML・CSS・JSで作る (Swiftスキル不要)

この投稿では、HTMLでmacOSのオリジナルのスクリーンセーバを作れる方法を紹介します。

macOSのスクリーンセーバを作ってみたいけど、SwiftもObjective-Cも分からない、分かるのはHTMLやCSS、JavaScriptなどウェブ関連技術だけ、というウェブ系プログラマむけの解説です。

ちなみに僕は、この投稿で紹介する方法で、JavaScriptの人気ライブラリをひたすら紹介するスクリーンセーバを作りました:

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">JavaScriptの人気ライブラリを紹介しまくるスクリーンセーバが完成した😌<br><br>需要があれば公開するかも <a href="https://t.co/tOnUhMIbh8">pic.twitter.com/tOnUhMIbh8</a></p>&mdash; suin❄️TypeScript入門書執筆中 (@suin) <a href="https://twitter.com/suin/status/1255862812640899074?ref_src=twsrc%5Etfw">April 30, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

Swiftなどを知らなくても、CSSやJavaScriptでアニメーションを頑張れば、こういった動きのあるスクリーンセーバが作れるわけです。

## スクリーンセーバをHTMLで作る手順

### Xcodeで新規プロジェクトを作る

まず最初に、スクリーンセーバのプロジェクトの下準備をしていきます。

Xcodeを起動して、新規プロジェクトを作ります。

<img width="802" alt="Blank_Skitch_Document.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/567ffc29-7194-0e2a-0d53-fb0d211f4817.png">

その際、「Screen Saver」のテンプレートを選びます:

<img width="730" alt="Screenshot_2020_04_29_13_45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/d77c6c80-d11b-c831-ca51-d5eac1d3f79f.png">

「Product Name」「Organization Name」「Organization Identifier」は適当に:

<img width="730" alt="Screenshot_2020_04_29_13_48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/2e2072c3-f24e-9f1b-a89f-5bfe10f6a4d8.png">

### Swiftの接着剤コードを準備する

macOSは標準でHTMLだけでスクリーンセーバが作れるわけではないので、HTMLをスクリーンセーバ上に描画するためのコードは、Swiftで書いておく必要があります。

ここでは、スクリーンセーバとHTMLの接着剤となるSwiftのコードを用意する手順を説明していきます。

まず、先程新規作成したプロジェクトから、不要なObjective-Cのファイルを消します:

<img width="399" alt="MyScreenSaverView_h.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/69067eaa-d3a6-b345-2053-2d7d8d3d7079.png">

代わりに、消したファイルと同じファイル名 + `.swift`で空のファイルを作ります。「MyScreenSaver」を右クリックして「New File」をクリックします。

<img width="393" alt="Screen Shot 2020-05-01 at 15.56.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/af08e030-7862-0a29-507d-d4b0642a3d78.png">

ファイル形式は「Swift File」にします:

<img width="730" alt="Screenshot_2020_05_01_15_57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/d069b401-9f8e-cdcd-1c19-eed802f997a9.png">

ファイル名は「MyScreenSaver<strong>View</strong>.swift」にします:

<img width="839" alt="Banners_and_Alerts.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/edc28953-7d73-a62e-5b82-2edc6730619f.png">

作成時に↓と聞かれますが、「Don't Create」を選びます:

<img width="558" alt="Screenshot_2020_05_01_16_00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/186a11be-1ff5-9fab-4dda-357f7a4f465d.png">


Swiftファイルが作成されたら、デフォルトで入っているコードは消し、下記コードをコピペして上書き保存します:

```swift:MyScreenSaverView.swift
import ScreenSaver
import WebKit

class MyScreenSaverView: ScreenSaverView, WKNavigationDelegate {
    private var webView: WKWebView!

    convenience init() {
        self.init(frame: .zero, isPreview: false)
    }

    override init!(frame: NSRect, isPreview: Bool) {
        super.init(frame: frame, isPreview: isPreview)
        setupWebView()
    }

    required public init?(coder: NSCoder) {
        super.init(coder: coder)
        setupWebView()
    }

    private func setupWebView() {
        let webConfiguration = WKWebViewConfiguration()
        // webkitがローカルファイルを参照できるようにする設定
        webConfiguration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
        // webViewをスクリーンの大きさに合わせる
        webView = WKWebView(frame: NSRect(x: 0, y: 0, width: frame.width, height: frame.height), configuration: webConfiguration)
        webView.navigationDelegate = self
        // webViewの背景を透明にする
        webView.setValue(false, forKey: "drawsBackground")
        // スワイプでの戻る/進むを無効にする
        webView.allowsBackForwardNavigationGestures = false
        // ピンチで拡大/縮小できる機能を無効にする
        webView.allowsMagnification = false

        // ローカルのHTMLを読み込む
        if let htmlPath = Bundle(for: type(of: self)).path(forResource: "html", ofType: nil) {
            let htmlUrl = URL(fileURLWithPath: htmlPath, isDirectory: true)
            let indexUrl = URL(fileURLWithPath: htmlPath + "/index.html", isDirectory: false)
            webView.loadFileURL(indexUrl, allowingReadAccessTo: htmlUrl)
        }
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        // ページが遷移したタイミングでwebViewをスクリーンセーバに表示する
        self.addSubview(webView)
    }

    // マウスが動いたときにスクリーンセーバが解除されるようにする(その1)
    override func hitTest(_ aPoint: NSPoint) -> NSView? {
        self
    }

    // マウスが動いたときにスクリーンセーバが解除されるようにする(その2)
    override var acceptsFirstResponder: Bool {
        true
    }
}
```

このSwiftコードが何をしているか簡単に説明すると、スクリーンセーバが起動したらWebView(埋め込み版のSafariのようなもの)を起動し、`html/index.html`を読み込み、それをスクリーンセーバ上に表示するといった処理が書かれています。

Swift側の準備は以上です。

### HTMLファイルを作る

次に、スクリーンセーバのグラフィックデザインを実装していく場所である`index.html`を作っていきます。

まず、`html`フォルダを作りたいので、「MyScreenSaver」フォルダを右クリックして「Add Files to "MyScreenSaver"」をクリックします:

<img width="444" alt="Screen Shot 2020-05-01 at 16.09.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/b0b25bc6-0f6d-a4c7-9436-dbe5eaad72bc.png">

すると、Finderのダイアログが出てくるので、「New Folder」をクリックし、「html」フォルダを作ったら「Add」をクリックして閉じます。

<img width="799" alt="Screenshot_2020_05_01_16_11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/f5bda91d-6747-507b-ca50-8efc4c1134ed.png">

次に、index.htmlを作るために、htmlフォルダを右クリック、メニューから「New File」を選びます:

<img width="452" alt="Screen Shot 2020-05-01 at 16.13.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/5090709f-d591-9edd-5c7d-8d15f38436e2.png">

「File」というファイルが作られるので、「index.html」に名前を変えておきます。

index.htmlの中身は適当に何か書いておきます:

```html/index.html
<style>
h1 {
  color: black;
  background: white;
  font-size: 30vh;
  animation: blink 1s step-end infinite;
}
@keyframes blink {
  75% { opacity: 0.0; }
}
</style>
<h1>Hello World</h1>
```

以上でHTMLファイルの準備は完了です。


### スクリーンセーバを実行できるように設定する

続いて、スクリーンセーバを実行するための設定をしていきます。

Xcodeのメニューバーにある「MyScreenSaver」をクリックするとメニューが出るので、そこから「Edit Scheme」を選びます:

<img width="371" alt="Screen Shot 2020-05-01 at 16.21.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/442224c2-cebc-581e-76c6-6049e6c3f6f6.png">

「Run」の「Info」タブの「Executable」セレクトメニューから「Other」を選び、ScreenSaverEngine.appに設定します:

<img width="892" alt="Screenshot_2020_05_01_16_25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/a8a68ff1-5fb9-ade4-d4d1-ab9a5b80c589.png">

ScreenSaverEngine.appは`/System/Library/CoreServices/ScreenSaverEngine.app`にあります。macOSのバージョンによって、ありかが異なる場合があります。

次に、「Arguments」タブを開き、

* `-window`
* `-module "$(EXECUTABLE_NAME)"`

の引数設定を追加します:

<img width="892" alt="Screenshot_2020_05_01_16_27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/d384f597-db92-d4fa-5cfa-bbd18e84224f.png">

更に、実行ごとにスクリーンセーバを自動的にインストールするように設定します:

<img width="1140" alt="Screen_Shot_2020-05-01_at_16_39_12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/9c71250d-b88e-f971-f58d-6732a2a9265b.png">

```
open -W "${CODESIGNING_FOLDER_PATH}"
```

以上で、スクリーンセーバを実行するための設定は完了です。

### スクリーンセーバを実行してみる

準備が整ったので、スクリーンセーバを実行してみましょう。

スクリーンセーバを実行するには、<kbd>Command</kbd>+<kbd>R</kbd>を押すか、XcodeのRunボタン(▶アイコン)を押します。

実行が開始すると、設定アプリが立ち上がり、スクリーンセーバをインストールするか聞かれるので「Install」を選択します:

<img width="666" alt="Screen Shot 2020-05-01 at 16.46.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/b09830da-4dde-fe8f-45f0-2ff7844e43b7.png">

すると、スクリーンセーバがインストールされ、スクリーンセーバの選択画面にプレビューが表示されるようになります。

<img width="668" alt="Screen Shot 2020-05-01 at 16.47.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/8431c005-fd6f-d9e1-8cca-66b578bc5728.png">

この状態で、設定アプリを閉じると、今度はスクリーンセーバを確認するためのウィンドウが表示されます:

<img width="800" alt="saver_window.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/055d4172-e603-c0c3-bf03-8c2f95dd8272.png">

index.htmlに書いた内容が表示されるはずです。

あとは、index.htmlを作り込んでいくと、オリジナルのスクリーンセーバが作れます。

ちなみに、実行したときに、インストールされたスクリーンセーバは、実行が終了しても残るので、設定アプリで自分が作ったスクリーンセーバを選択しておけば、Xcodeを閉じてもそのスクリーンセーバが使われるようになります。

## Safariを使った動作確認がおすすめ

HTMLを直してはXcodeの「Run」でスクリーンセーバをインストールし直して確認する、というのは開発体験としてかったるいので、HTML側にごりごり書いていくときはSafariで動作確認するのをおすすめします。Safariを使うと、ウェブインスペクタも使えるのでデバッグがはかどります。

ChromeやFirefoxではなくSafariを使う理由は、Swiftのコードで使っている`WKWebView`が他ブラウザの環境よりSafariに近いからです。


普段、Swiftを書かない人は、Xcodeも慣れていないと思うので、HTMLをいじるときはVSCodeなど自分の好きなエディタを使ってもいいです。

Safariで動作確認するときは、`html/index.html`を開くだけです。HTMLを直したら、リロードする。普段のウェブアプリの開発と同じ要領です。

スクリーンセーバの作りにもよりますが、`fetch()`などで外部APIを叩いたりするときは、Same Origin Policyにひっかかってしまうことがあります。その場合は、Safariの「Devloper」→「Disable Cross-Origin Restrictions」をオンにしておくと良いです:

<img width="648" alt="Screen Shot 2020-05-01 at 16.56.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/a9736b92-733f-086d-fa4f-ca31c212f860.png">

また、フルスクリーンでの動作確認をするときは、「View」→「Always Show Toolbar in Full Screen」のチェックを外しておくと良いです:

<img width="599" alt="Screen Shot 2020-05-01 at 16.59.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/06d7bffe-d18d-2472-f786-a4a280fea4b8.png">

## おわり

macOSのスクリーンセーバを作るのに、Swiftの知識はほぼ不要です。

慣れ親しんだHTMLやJavaScriptを駆使して、自分だけのスクリーンセーバを作ってみてはいかがでしょうか。

---

最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローしてもらえると嬉しいです:relieved:→[Twitter@suin](https://twitter.com/suin)