Hello, Swift ラバーなみなさん。これからラバーのみなさん。

2022 年の iOSDC2022 の発表の中で、まつじさんという方の発表がありました。

なんとその発表スライドが SwiftUI で作られているというではありませんか。

発表を見た私の感想は・・「なにそれすごい(小並感)」というものでした。

しかもすでにSlideKitというライブラリにされているではないですか!!

これは使ってみるしかねぇ!!

ということでやってまいります。

SlideKit とは

SlideKit は、SwiftUI でプレゼンテーション スライドを作成するのに役立ちます。

すべてのコンポーネントが SwiftUI の View であるため、簡単にプレゼンテーション スライドを作成し、デザインを完全にカスタマイズできます。

SlideKit README

なるほどー。

なんかすごいぞ(小並感)

とりあえず使ってみる

まずは、公式のチュートリアルに沿って、作ってみましょう!

(全部を紹介すると、なかなかの量になるので、気になる方は試してみてください!)

環境構築

まずは Swift 製のパッケージ管理ツールの「mint」を導入しましょう。

(homebrew が入っている前提です、他にも導入方法があるようなのでお好みのものを選択してください)

今回使うバージョン(2022 年 11 月時点)

  • MacOSX: 12.6.1
  • Xcode: 14.1
  • Homebrew: 3.6.7
$ brew install mint

$ mint --version

Version: 0.17.2

インストールができましたら、SlideKit のプロジェクトファイルを作成しましょう。

まずは作成先のディレクトリに移動します。

$ cd <プロジェクトのディレクトリ>

今回は Home ディレクトリで「~/Develop/SlideKit_Demo」というディレクトリを作成したので、下記のようになりました。

$ cd ~/Develop/SlideKit_Demo

実際にプロジェクトを作成します。

$ mint run mtj0928/SlideGen <保存名> --platform <対象 OS>

(mtj0928/SlideGen はまつじさんが作られたスライドアプリを作るライブラリ)

今回は、

  • プロジェクト名を「SlideKitDemo」
  • 対象 OS はチュートリアルを参考に「macOS」

としました。

$ mint run mtj0928/SlideGen SlideKitDemo --platform macOS

完了したら、スライドの Xcode プロジェクトができていると思うので、開いてみましょう。

開けたら、まず実行します(⌘+R)!

こんな感じにサンプルのスライドが表示されました!

(以下のサンプルはスクショが見やすいようにbackgroundColorを設定しています。通常は白です。)

ここからスライドを追加していきます。

最初のスライド

Slides ディレクトリ配下にIntroductionSlide.swiftという SwiftUI ファイルを作成します。

開いたら、Slide を作成する前に 3 つ、やるべきことがあります。

  1. SlideKit を import する
  2. Slide protocol に準拠する
  3. SlidePreview で表示する Preview内のView を囲う

こちらコードです

import SwiftUI
import SlideKit // ①

struct IntroductionSlide: Slide { // ②
    var body: some View {
        Text("Hello, World!")
    }
}

struct Slide_Previews: PreviewProvider {
    static var previews: some View {
        SlidePreview { // ③
            IntroductionSlide()
        }
    }
}

さぁ、準備は整いました!

次にスライドをカスタムしてきましょう。

body 部分にHeaderSlide() {}を記述します。

いわゆるタイトルテキストですね!

var body: some View {
    HeaderSlide("SlideKit") {}
}

Preview がこんな感じになりましたでしょうか?

Simulatorで確認するには、SlideConfiguration.swift内のslideIndexController変数内にIntroductionSlide()を追加してください。

let slideIndexController = SlideIndexController {
   SampleSlide()
   IntroductionSlide()
}

次にIntroductionSlide.swiftHeaderSlide()のコールバック内にItem()を記述します。

HeaderSlide("SlideKit") {
    Item("SlideKit helps you make presentation slides by SwiftUI.")
    Item("The followings are provided.")
}

いわゆる箇条書きですね!

こんな簡単にかけるなら、作成も楽そうです!

このように、想像より(?)簡単だったのではないでしょうか?

では、チュートリアルの外に出て、通常のスライド作成ソフトでは作りづらいようなものを作ってみましょう。

デモスライドを作ってみる

今回は以下のようなスライドを作りました。

6枚のスライドですが、SwiftUI の機能をしっかり盛り込んでいます。

構成

1. 段階表示

2. アプリ埋め込み

3. カスタムデザイン

4. WebView

5. Charts API

6. コード表示

(*注意:以下、記事の都合上、一部本筋と関係のないコードは省略しています。実際に動作確認する場合は、そのままだと動かないので、適宜置き換えてください。)

また、趣向を変えて、iPad用にプロジェクト作成を行ったため、少し構築手順が異なります。

環境構築

任意のディレクトリにて、以下を実行しました。

$ mint run mtj0928/SlideGen SlidekitDemoProject --platform iOS

前述のチュートリアルでは、作成したスライドの追加をSlideConfiguration.swiftに追加をしましたが、iOSプロジェクトの場合、SceneDelegate.swiftslideIndexController変数に追加していきます。

    static let slideIndexController = SlideIndexController(index: 0) {
        TitleSlide()
        AppDemoSlide() // このように作成したスライドを表示させたい順で追加する
    }

プロジェクトが作成されましたら、実際に作っていきましょう!

1. 段階表示

1 ページ目はタイトルスライドです。

世のスライドではよくある、順番にコンテンツを表示させる機能です。

チュートリアルにもありますが、IntPhasedStateに準拠した enum を使用します。

// 列挙体名は固定、IntとPhasedStateに準拠する
enum SlidePhasedState: Int, PhasedState {
    case initial, second, third // ここの列挙子名はなんでもいい
}
@Phase var phasedStateStore // ここの変数名も固定

enum の列挙子名については任意ですが、列挙体名はSlidePhasedStateである必要があります。

また、@Phaseの propertyWrapper の付いた変数名もphasedStateStore固定になります。

スライドをクリックするごとに enum の値が増えていくようなイメージです。

クリック時に動作する段階表示制御にもいくつか種類あります。

  • 今回使っている.after()だと、そのケース以降は表示する
  • .when()だと、そのケース時だけ表示する

他にもありますので、ソースを見てみてください!

これらの使い方は、実際のソースを見るとわかりやすいかもしれません。

struct TitleSlide: Slide {

    enum SlidePhasedState: Int, PhasedState {
        case initial, second, third
    }
    @Phase var phasedStateStore

    var body: some View {
        VStack {
            Text("SwiftUI")
                .font(.system(size: 90, weight: .heavy))

           // 画面を一回クリックすると表示されるようになる。それ以降表示される
            if phasedStateStore.after(.second) {
                Text(" で作るn")
                    .font(.system(size: 70, weight: .heavy))
            }
  
            // 画面を二回クリックすると表示されるようになる。(今回それ以上ないけど)それ以降表示される
            if phasedStateStore.after(.third) {
                Text("SlideKit")
                    .font(.system(size: 190, weight: .heavy))
                    .padding(.top, -100)
            }
        }
        .frame(maxWidth: .infinity, alignment: .center)
        .padding(90)
    }
}

画面をクリックするたびにphasedStateStoreの値が、initialsecondthirdと増えていくイメージです。

2. アプリ埋め込み

2 ページ目はアプリ埋め込みを行ったスライドです。

やり方は簡単、好きなアプリを埋め込むだけです・・・

それだとお話にならないので、今回は簡単なチャット風アプリを仕込んでみました。

struct AppDemoSlide: Slide {
    var body: some View {
        HeaderSlide("デデモアプリを埋め込んでみた") {
            HStack(alignment: .top) {
                DemoApp()
                    .padding(.trailing, 100)

                DescriptionView()
                    .padding(.top)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
            .padding(.horizontal, 40)
        }
    }

    @ViewBuilder
    func DemoApp() -> some View {
        ChatView() // 埋め込んだサンプルアプリ
            .frame(width: 500)
            .overlay {
                RoundedRectangle(cornerRadius: 50)
                    .stroke(Color.black, lineWidth: 10)
            }
            .clipShape(RoundedRectangle(cornerRadius: 50))
    }

    @ViewBuilder
    func DescriptionView() -> some View {
        VStack(alignment: .leading) {
            Item("なんとアプリを埋め込める")
            Item("アプリなので、動かせちゃう")
        }
    }
}

普段の SwiftUI プロジェクトと同じように、宣言するだけですね!

本筋ではないですが、Chat画面のコードはこちらです。(一部のコードは省略)

struct ChatView: View {
    @State private var messages: [Message] = []
    @State private var currentMessageNumber: Int = 0

    var body: some View {
        ZStack {
            VStack(spacing: 0) {
                Spacer()
                    .frame(height: 40)

                GeometryReader { proxy in
                    ScrollView {
                        LazyVStack(spacing: 20) {
                            ForEach(messages) { message in
                                MessageItem(message: message, viewWidth: proxy.size.width)
                                    .padding(.horizontal)
                            }
                        }
                    }
                }
            }

            VStack {
                Spacer()

                Button {
                    messages.append(sampleMessages[currentMessageNumber])
                    if currentMessageNumber == sampleMessages.count - 1 {
                        currentMessageNumber = 0
                        return
                    }
                    currentMessageNumber += 1

                } label: {
                    Text("Message Add")
                        .font(.title.bold())
                        .foregroundColor(.white)
                        .padding(20)
                        .background(.blue)
                        .cornerRadius(12)
                        .padding(.bottom, 20)
                }
            }
        }
        .background(.green.opacity(0.2))
    }

    @ViewBuilder
    func MessageItem(message: Message, viewWidth: CGFloat) -> some View {
        let isReceived = message.type == .received
        Text(message.text)
            .font(.system(size: 24))
            .padding(.horizontal)
            .padding(.vertical, 12)
            .background(isReceived ? .black.opacity(0.2) : .green.opacity(0.9))
            .cornerRadius(13)
            .frame(width: viewWidth * 0.7, alignment: isReceived ? .leading : .trailing)
            .padding(.vertical)
            .frame(maxWidth: .infinity, alignment: isReceived ? .leading : .trailing)
    }

実際のアプリを埋め込むなら、package とかで読み込むのがいいかもしれないですね!

3. カスタムデザイン

デフォルトでもいい感じのスライドですが、スライドデザインをカスタムすることもできます。

今回は縦書きでスライドを作ってみました。

TategakiText は、NSAttributedStringUIViewRepresentable でラップしてゴニョゴニョしています。

実は先程までのHeaderSlideと違い、String ではなく、View の構造体を Header タイトルに設定しています。

String以外を使うにはライブラリを拡張する必要があるかなーと思っていたら、すでに用意されていました!!

デザインカスタムするには、HeaderSlideStyleに準拠した構造体でデザインを実装します。

今回はCustomHeaderという構造体を実装しました。

HeaderSlideStyleで宣言されているfunc makeBody(configuration:)を使用して実際に実装します。

header と content は configuration という引数で渡されてくるので、そちらに対し、効果を与えていきます。

そして、実際に使用する際には、スライドビューに対し、.headerSlideStyle(CustomHeader())のように適用します。

すべてのスライドに適用したい場合は、main ファイルのvar presentationContentView内にあるSlideRouterView()に適用すると反映されます。

では、実際のコードです。

struct CustomHeaderView: Slide {
    var body: some View {
        HeaderSlide {
            TategakiText(text: "縦書きのヘッダーもいいね", fontSize: 80, fontColor: .white)

        } content: {
            HStack(spacing: 0) {
                TategakiText(text: description, fontSize: 44)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
        .headerSlideStyle(CustomHeader())
    }

    let description = "..." //省略
}

struct CustomHeader: HeaderSlideStyle {
    func makeBody(configuration: Configuration) -> some View {
        HStack(alignment: .top, spacing: 100) {
            configuration.content
                .padding(100)

            configuration.header
                .frame(width: 400)
                .frame(maxHeight: .infinity)
                .padding(.top, 40)
                .background(.red)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

簡単にスライドの世界観を統一できそうですね!

4. WebView

次に WebView です。

こちらも先ほどと同様にただ画面内に宣言するだけです。

WebView自体もただ表示するだけのサンプルです。

実装で特に難しいものはないですね!!

struct WebViewSlide: Slide {
    var body: some View {
        HeaderSlide("WebViewだって表示できます") {
            WebView(url: "https://up-frontier.jp/")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }
}

struct WebView: UIViewRepresentable {
    let url: String

    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()

        let request = URLRequest(url: URL(string: url)!)
        webView.load(request)

        return webView
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {}
}

5. Charts API

次に Charts API を使ったものです。

こちらは Xcode14 からのものなので、注意してください。

とはいえ、スライドとしてやれることは、他の画面と変わりません。

まずはスライド部分

struct ChartDemoSlide2: Slide {

    var body: some View {
        HeaderSlide("チャートも書けちゃう") {
            VStack(alignment: .leading) {
                Item("Xcode14からのChartsAPIを使いました")
                Item("2000年から2022年の平均ドル円相場です")

                ChartView()
            }
        }
    }
}

シンプルですね〜

本筋とは関係ないですが、Chart のサンプルも貼っておきます。

struct ChartView: View {
    @State private var rates: [DollarYen] = sampleRate

    var body: some View {
        Chart {
            ForEach(rates) { rate in
                BarMark(
                    x: .value("Date", dateFromString(string: rate.date), unit: .year),
                    y: .value("Yen", rate.isAnimate ? rate.yen : 0)
                )
                .foregroundStyle(.blue.gradient)
                .interpolationMethod(.catmullRom)

                AreaMark(
                    x: .value("Date", dateFromString(string: rate.date), unit: .year),
                    y: .value("Yen", rate.isAnimate ? rate.yen : 0)
                )
                .foregroundStyle(.green.opacity(0.3).gradient)
            }
        }
        .onAppear {
            for (index, _) in rates.enumerated() {
                DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.05) {
                    withAnimation(.interactiveSpring(response: 0.8,
                                                     dampingFraction: 0.8,
                                                     blendDuration: 0.8)) {
                        rates[index].isAnimate = true
                    }
                }
            }
        }
    }
}

onAppear()の部分は、ただビヨーンとアニメーションをしたかっただけなので、

チャート自体には関係ありません。

6. コード表示

やっぱり、プログラマがスライド作るなら、コードを載せたいですよね!

簡単です!

Codeという構造体に String 形式でコードを渡すだけです!

var body: some View {
    ScrollView {
        Code(code, colorTheme: .defaultDark, fontSize: 36)
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
            .padding(48)
            .background(Color.init(red: 41 / 255, green: 42 / 255, blue: 47 / 255))
    }
    .padding()
}
let code = "..." // 省略

Code の引数にある、colorTheme:は、スニペットの文字色が dark モード対応になります。

現状、背景色には適用できないようなので、通常通り背景色を設定します。

まとめ

このようにすごい簡単にオリジナルスライドを作成することができました!

なかなか登壇などの発表の機会は少ないですが、チャンスがあれば使ってみたいと思います!

それでは、素敵な Swift ライフを!!



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる