SwiftUIでプレゼンスライドが作れるSlideKit

# はじめに
プレゼンスライドを作る時、皆さんは何で作りますか？
Keynoteでしょうか？それともPowerPoint？
僕も以前はPowerPointを愛用していました。
ただエンジニアをしている身としてはやはりコードでスライドを実装してみたいものです。
そんな思いに駆られてSwiftUIでプレゼンスライドが作れる[SlideKit](https://github.com/mtj0928/SlideKit)というライブラリを作りました。
この記事ではSlideKitの使い方を解説します。

![190956930-ea9ce4d0-0a19-4bb3-b43b-28dd2d73374a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/a6699a57-8a97-f504-6e99-f4c172a52561.png)


# 基本的な使い方
## セットアップ
さて、まずはスライドを表示するアプリを用意する必要があります。
ここではSlideKitを使ったスライドアプリを生成するために、[SlideGen](https://github.com/mtj0928/SlideGen)を使います。
SlideGenはmintで簡単に使えるようになっています。
(ちなみにこのSlideGenも自分で実装しました)
```sh
$ mint run mtj0928/SlideGen SlideKitPresentation --platform macOS
```

このコマンドを実行すると、スライドアプリのテンプレートが`SlideKitPresentation`というxcodeprojとして生成されます。
`platform`に渡している`macOS`を`iOS` に変更するとiOS版のxcodeprojが生成されますが、この記事ではmacOSを中心に説明していきます。

さて早速できたプロジェクトをビルドしてみると、こんな感じの画面が表示されます。
![Slide1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/d8ed2ad7-d258-a39e-8aed-eb72a775cc73.png)

もしかしたら端に黒い部分があるかもしれませんが、ウインドウの大きさを変えると自動で適切なアスペクト比になるはずです。

これでセットアップは完了です。
ここから実際にスライドを作っていきます。
## スライドの作り方
まずは簡単なスライドを作っていきます。
`IntroductionSlide.swift`というファイルを追加して、以下のようにします。
```swift
import SwiftUI
import SlideKit

struct IntroductionSlide: Slide { // ① 準拠するのはViewではなくSlide
    var body: some View {
        // ② HeaderSlideは一番上にタイトルがあるスライドのテンプレート
        HeaderSlide("SlideKit") {
            // ③ Itemを使うと楽に箇条書きができる
            Item("SlideKitはSwiftUIでスライドが作れるライブラリです")
            Item("ネストしたり、数字で箇条書きにもしたりもできます") {
                Item("1つ目のアイテム", accessory: 1) {
                    Item("ネストのネストもできます", accessory: "a")
                }
                Item("2つ目のアイテム", accessory: 2)
                Item("3つ目のアイテム", accessory: 3)
            }
        }
    }
}

struct IntroductionSlide__Previews: PreviewProvider {
    static var previews: some View {
        // ④ XcodePreviewを使う時はSlidePreviewをで囲むと適切な形になります
        SlidePreview {
            IntroductionSlide()
        }
    }
}
```

SlideKitで作成しているスライドはXcodePreviewで確認が可能です。
上のコードの場合、このようになるはずです。

![SimpleSlide.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/0f0c4fa2-dab1-8f4e-1edc-a64f09b765be.png)

コードの中にコメントを入れてあるので、大体内容はわかると思いますが、`HeaderSlide`と`Item`で箇条書きのスライドを作っています。

## スライドをアプリに追加する
さて、上で作った`IntroductionSlide`はXcodePreviewでは確認できますが、まだアプリを実行しても表示されません。
アプリでスライドとして表示するには`SlideIndexController`に登録する必要があります。
SlideGenで生成されたプロジェクトの中に、`SlideConfiguration.swift`というファイルがあります。
そのファイルの中で`SlideIndexController`の生成をしているので、そこに先ほど追加した`IntroductionSlide`を追加します。
```swift
import SwiftUI
import SlideKit

struct SlideConfiguration {
    // ...省略
    let slideIndexController = SlideIndexController {
        IntroductionSlide()  // ← この一行を追加する
        SampleSlide()
    }
}
```
アプリをビルドすると、先ほど作った`IntroductionSlide`がアプリに表示されます。
この`SlideIndexController`に追加したスライドは上から順番に表示されます。
Returnキーを押したり右矢印キーを押したりすることで、スライドを次のスライドに遷移させることができます。
逆に左矢印キーを押すことで一つ前のスライドに戻ることができます。

## スライドを段階的に表示する
スライドで発表をする時に段階的にスライドを表示したくなることがあります。
SlideKitはそのようなユースケースをカバーしています。
先ほど作成した`IntroductionSlide`に戻って、次のように編集します。
```swift
struct IntroductionSlide: Slide {    
    enum SlidePhasedState: Int, PhasedState { // 型名が違うとコンパイルエラーになるので注意
        case initial, secondItem, all
    }

    @Phase var phasedStateStore: PhasedStateStore<SlidePhasedState> // 変数名が違うとコンパイルエラーになるので注意
   
    var body: some View {
        //省略
    }
}
```

表示する段階を`SlidePhasedState`というenumで表現しています。
`SlidePhasedState`が準拠している`PhasedState`は`.initial`を要求しています。
これはスライドが表示される際の最初の段階になります。
そして、`phasedStateStore`には現在の`SlidePhasedState`があります。
`phasedStateStore`には`after(_)`や`when(_)`といったBoolを返すメソッドがあるので、これらを用いて`body`の中身を現在の状態に合わせて分岐することができます。
```swift
var body: some View {
    if phasedStateStore.when(.secondItem) { 
        // 現在の状態がsecondItemの時だけここに入る
    }
    if phasedStateStore.after(.secondItem) {
        // 現在の状態がsecondItem以降の時にここに入る
    }
}
```

さて、先ほど追加した `IntroductionSlide`に追加します。
```swift
struct IntroductionSlide: Slide {
    // ... 省略
    var body: some View {
        HeaderSlide("SlideKit") {
            Item("SlideKitはSwiftUIでスライドが作れるライブラリです")
            if phasedStateStore.after(.secondItem) {
                Item("ネストしたり、数字で箇条書きにもしたりもできます") {
                    if phasedStateStore.after(.all) {
                        Item("1つ目のアイテム", accessory: 1) {
                            Item("ネストのネストもできます", accessory: "a")
                        }
                        Item("2つ目のアイテム", accessory: 2)
                        Item("3つ目のアイテム", accessory: 3)
                    }
                }
            }
        }
    }
}
```
これをビルドして、スライドを進めることで、段階的に表示されていることがわかると思います。

![スクリーンショット 2022-10-22 11.09.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/2938a046-8a59-cb18-ecf9-58420eb77794.png)
![スクリーンショット 2022-10-22 11.09.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/72f11a1a-3672-e720-c369-4950ecae00e4.png)
![スクリーンショット 2022-10-22 11.09.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/5d1d227b-66dc-df01-7a12-339519baec04.png)

## 発表者ツールを使う
発表をする時はスクリプトが見たいですよね。
SlideKitはこのようなユースケースもサポートしています。
`Slide`プロトコルには`script`というプロパティがあります。
デフォルトでは空の文字列なのですが、カスタムすることで発表中にスクリプトを見ることができます。
```swift
struct IntroductionSlide: Slide {
    // ... 省略
    var script: String {
        switch phasedStateStore.current {
        case .initial:
            return """
            SlideKitについて紹介します
            SlideKitはSwiftUIでスライドが作れるライブラリです
            今皆さんが見ているようにヘッダーを持つスライドを作れ、箇条書きにも対応しています。
            """
        case .secondItem:
            return """
            さらに実はネストしたり、数字の箇条書きしたりできます。
            """
        case .all:
            return """
            こんな感じですね
            """
        }
    }
}
```
そしてこのスクリプトはアプリを起動して`⌘ + P`を押すことで確認できます。
![presenterview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/38bbf351-a3d7-1c73-cf7c-ffc819418e4a.png)

プレゼンしているウインドウとは別のウインドウとして表示されるので、Zoomなどで発表する時は発表スライドのウインドウだけを画面共有すれば、スクリプトが見られる心配はありません。

## ページ番号を隠す
表示されているスライドの右下にページ番号があることに気づきましたか？
このページ番号は自動でSlideKitが表示しています。
しかし、タイトルスライドのような、ページ番号を表示したくないこともあると思います。
`Slide`プロトコルには`shouldHideIndex`プロパティがあります。
このプロパティはデフォルトでは`false`ですが、この値を`true`にすることで、ページ番号を隠すことができます。
```swift
struct IntroductionSlide: Slide {
    // ... 省略
    var shouldHideIndex: Bool {
        true
    }
}
```

## ソースコードを表示する
発表スライドにソートコードを表示したいときもあると思います。
SlideKitには`Code`というViewが用意されています。
Swiftのみですが、`Code`はシンタックスハイライトをサポートしています。
(他の言語をサポートするPR待っています！) 

```swift
struct CodeSlide: Slide {
    var body: some View {
        HeaderSlide("Code Slide") {
            Text("シンタックスハイライトもサポート！")
            Code(code, fontSize: 32)
        }
    }
}

private let code = """
struct CodeSlide: Slide {
    var body: some View {
        HeaderSlide("Code Slide") {
            Text("シンタックスハイライトもサポート！")
            Code(code, fontSize: 32)
        }
    }
}
"""

struct CodeSlide_Previews: PreviewProvider {
    static var previews: some View {
        SlidePreview {
            CodeSlide()
        }
    }
}
```
![スクリーンショット 2022-10-22 17.29.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/e3ea18ba-16e3-383c-5077-0c3b68de2187.png)

また、ダークテーマも用意してあります。(背景色もそのうち足します😅)
```swift
Code(code, colorTheme: .defaultDark, fontSize: 32)
    .frame(maxWidth: .infinity, alignment: .leading)
    .padding(48)
    .background(Color.init(red: 41 / 255, green: 42 / 255, blue: 47 / 255))
```

![スクリーンショット 2022-10-22 17.37.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/e02169b5-e578-6a14-6ed2-9b629b6dda97.png)

# スライドのテーマを変更する
`HeaderSlide`や`Item`など、便利な機能を持つSlideKitですが、やはりずっと同じデザインだと飽きますし、オリジナリティもないです。
SlideKitでは以下の3つのデザインを自由に変更できる機能があります。
- `HeaderSlide`
- `Item`
- ページ番号

ここでは`HeaderSlide`のデザインを変更していきます。
まずは簡単なスライドを用意します。
```swift
struct CustomThemeSlide: Slide {
    var body: some View {
        HeaderSlide("スライドのテーマ") {
            Item("SlideKitはカスタムテーマに対応しています")
            Item("HeaderSlideStyleを使います")
        }
    }
}

struct CustomThemeSlide_Previews: PreviewProvider {
    static var previews: some View {
        SlidePreview {
            CustomThemeSlide()
        }
    }
}
```
このスライドだと次のようなデザインになります。
![スクリーンショット 2022-10-22 21.09.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/2c039f57-c243-f99d-8f72-5f0969c4b93a.png)

まずは`CustomHeaderSlideStyle`を追加します。
```swift
struct CustomHeaderSlideStyle: HeaderSlideStyle {
    func makeBody(configuration: Configuration) -> some View {
        VStack {
            configuration.header
            configuration.content
        }
    }
}
```
そしてこれを先ほどの`CustomThemeSlide_Previews`に追加します。
```swift
struct CustomThemeSlide_Previews: PreviewProvider {
    static var previews: some View {
        SlidePreview {
            CustomThemeSlide()
        }
        .headerSlideStyle(CustomHeaderSlideStyle()) // この行に追加する
    }
}
```
するとXcodePreviewはこうなります。
![スクリーンショット 2022-10-22 21.13.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/5f518404-22fe-2ecd-35a4-c6f313f39a99.png)

無茶苦茶なデザインですね。大丈夫です。
ここで`CustomHeaderSlideStyle`について解説します。
```swift
struct CustomHeaderSlideStyle: HeaderSlideStyle {
    func makeBody(configuration: Configuration) -> some View {
        VStack {
            configuration.header // ① スライドのヘッダー
            configuration.content // ② スライドのコンテンツ
        }
    }
}
```
SwiftUIの`LabelStyle`と同じ発想で実装されています。
`configuration`には`HeaderSlide`のヘッダーとコンテンツが入っています。
これを元にスライドのデザインをいじっていきます。
```swift
struct CustomHeaderSlideStyle: HeaderSlideStyle {
    func makeBody(configuration: Configuration) -> some View {
        HStack(alignment: .center) {
            configuration.header
                .font(.system(size: 60, weight: .black))
                .frame(width: 640)
                .frame(maxHeight: .infinity, alignment: .leading)
                .foregroundColor(.white)
                .background(.red)
            VStack(alignment: .leading, spacing: 60) {
                configuration.content
                    .font(.system(size: 48))
            }
            .padding(60)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
    }
}
```
この実行結果は次のようになります。
![スクリーンショット 2022-10-22 21.25.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/d660d88e-2716-16e7-cba9-3e9ae4deb5cd.png)

さっきとは全然違うデザインになりましたね。
ここでは`HeaderSlide`のデザインを変更しましたが、同様に`Item`のデザインは`ItemStyle`を、ページ番号のデザインは`IndexStyle`をカスタマイズしていきます。

ところでこの`CustomHeaderSlideStyle`を各スライドに対して適用していくのは大変ですよね。
スライド全体に適用するには、SlideGenが生成した`SlideKitPresentationApp`の`presentationContentView`というプロパティに適用するとスライド全体に適用されます。
(`SlideKitPresentationApp`の正確な名前はSlideGenの生成時に渡した引数によって変わります。)

```swift
@main
struct SlideKitPresentationApp: App {
    // 省略...
    var presentationContentView: some View {
        SlideRouterView(slideIndexController: Self.configuration.slideIndexController)
            .headerSlideStyle(CustomHeaderSlideStyle()) // この一行を追加する
            .background(.white)
            .foregroundColor(.black)
    }
    // 省略...
}
```
上の一行を追加するだけで全てのスライドに適用され、既存のスライドもこのようにデザインが変わります。
![スクリーンショット 2022-10-22 21.33.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/d04fc4cc-ed93-a745-2e16-831a7e137752.png)
ガラッと印象が変わりましたね。

## タイトルスライドを実装する
ここまで、全て`HeaderSlide`を使ってスライドを実装してきましたが、別に`HeaderSlide`を使用することは必須ではありません。
SlideKitではSwiftUIが提供している様々なViewを組み合わせてスライドを作ることができます。
最後にタイトルスライドを実装していきます。

```swift
struct TitleSlide: Slide {
    var body: some View {
        VStack {
            titleText
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(90)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
        .background(.red)
        .foregroundColor(.white)
    }

    var titleText: Text {
        Text("SwiftUI")
            .font(.system(size: 90, weight: .heavy))
        + Text(" で\n")
            .font(.system(size: 70, weight: .heavy))
        + Text("プレゼンスライド")
            .font(.system(size: 90, weight: .heavy))
        + Text(" が ")
            .font(.system(size: 70, weight: .heavy))
        + Text("作れる\n")
            .font(.system(size: 90, weight: .heavy))
        + Text("SlideKit")
            .font(.system(size: 190, weight: .heavy))
    }

    var shouldHideIndex: Bool { true }
}
````
Text周りの操作は大変ですが、このスライドはこのような感じになります。
![スクリーンショット 2022-10-22 22.27.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70011/af901b1f-3302-d143-334a-745e3d792854.png)
このように`HeaderSlide`や`Item`を使わずにSwiftUIのViewを使うことでより自由にスライドのデザインができます。

# SlideKitの良いところ
## 1. スライドの使い回しができる！
PowerPointやKeynoteなどは一度見せたスライドをもう一度表示するには、スライドのコピペをする必要がありました。
ただ、そうするとどちらかのスライドを変更した時にもう一方も編集する必要が出てきて同期し続けるのが大変でした。(実際はできるのに自分が知らないだけかもですが...)
しかし、SlideKitのスライドは単なるstructなので、何回でも好きなだけ使いまわせる上、変更も一箇所ですみます。
また、スライドと同様に色の管理なども簡単にできます。

## 2. SwiftUIだから、実装してしまえばなんでもできる
単なるSwiftUIなので、実装さえしてしまえば、PowerPointやKeynoteではできないことも色々できます。
例えば以下のようなことができます

- アプリのデモをスライドの中に埋め込む
- 紹介したいWebページがあればWebViewで表示する
- Swift Chartsを使ったグラフの描画
- イベントのハッシュタグがあればそれをTwitterから拾ってきてスライド中に表示する
- Firebaseと連携してリアルタイムなアンケートなど

僕ではこの程度しか思いつきませんが、まだまだ皆さんのアイデア次第では色々なことをスライドの中に入れることができます。

# SlideKitの微妙なところ
## 1. 細かいデザインの調整が大変
SlideKitにはふき出しや矢印といったKeynoteやPowerPointには当たり前のように入っているデザインがまだありません。
また、そういったものを表示する時の位置の調整も直感的にはできないので、なかなか苦労します。

## 2. PDFに変換できない
PDFにしてスライドを共有することも多いと思いますが、まだSlideKitにはスライドをPDFにする機能がありません。
iOS16 / macOS13 から新しく追加された`ImageRenderer`を使えば、SwiftUIのViewを画像にして、PDFに生成することもできそうですが、文字情報などがどうしても欠落してしまいます。
何か良い方法があれば教えてください。

# 最後に
この記事ではSwiftUIでスライドが作れる[SlideKit](https://github.com/mtj0928/SlideKit)について紹介しました。
実はこの記事と同様のことをDocCの[チュートリアル](https://mtj0928.github.io/SlideKit/tutorials/meet-slidekit)としても公開しているので、もしより細かいステップで知りたい方がいましたらそちらも参考にしてください。

「SlideKitを使ってスライド作ってみた」という方がいましたらぜひ、教えてください！
[SlideKit](https://github.com/mtj0928/SlideKit)へのスター、コントリビュートもお待ちしています！