Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

ヤプリの R&D 領域で Apple Vision Pro への取り組み - お料理アプリ開発

はじめに

こんにちは、ヤプリでiOSエンジニアをしている菅(@Nao_RandDです。

前回に続いて、Apple Vision Pro に関する記事です。

前回記事:

tech.yappli.io

今年の夏、ヤプリでは R&D 領域の取り組みとして Apple Vision Pro を3台購入しました!(1台でなく3台!、すごいっ!🎉)

その取り組みのひとつで、ヤプリのクライアントに協力してもらい visionOS 向けにプロトタイプアプリを作成しました。

動画のような visionOS 向けお料理アプリになっています。

プロトタイプアプリの動作

今回は実装を交えて、そのプロトタイプアプリを紹介していきます。

前提

ヤプリではノーコードアプリプラットフォーム「Yappli」を提供しており、コードを書くことなくiOS, Android のネイティブアプリを作ることができます。

yapp.li

Yappli を利用して、「土井善晴の和食 - 旬の献立をレシピ動画で紹介」 を提供しているクライアントから許可をいただき、visionOS 向けのプロトタイプを作成しました。

ヤプリで提供している iOS アプリ

apps.apple.com

🚨:あくまでプロトタイプとして作成しており、visionOS アプリは App Store に公開されていません


以下のような点から、Apple Vision Pro とお料理アプリは非常に親和性の高いものと思い、プロトタイプアプリを visionOS 向けに作成しています。

  • UI 操作が視線と指の動きで完結するため、手が汚れていても操作ができる
  • 料理をしながら動画を再生したり、YouTube など他のコンテンツも楽しめる
  • 3Dオブジェクトから盛り付けや、完成品のイメージを提供しやすい

アプリ構成

それでは、作成したアプリの構成と実装をご紹介していきます。

ホーム画面

アプリのホーム画面です。レシピを選択して動画を再生するまでの役割を担います。

ホーム画面

visionOS らしい見た目となるように、Ornament(オーナメント)という表示でタブを切り替えられるようにしつつ、スクロールに応じて上部にあるアイキャッチ画像が縮小するようにしています。

Ornament は visionOS 向けのUIパーツで、関連するウィンドウよりも少し手前に上下左右どのエッジにも配置できます。

developer.apple.com

よく使うコントロールや情報を、ウインドウの邪魔にならない一貫した位置に提示するときにはオーナメントの使用を検討する、というのがベストプラクティスとしてドキュメントに記載してあります。

visionOS で TabView を利用すれば、デフォルトで Ornament で表示されるため特に複雑な実装は必要ありません。

スクロールに応じて上部のアイキャッチ画像が縮小する部分は visionOS 特有というより、SwiftUI の実装トピックなので主として扱うのは避けます。 実装は折りたたみ要素内で軽く紹介しておりますので、興味のある方は覗いていただければと思います。

↓↓ ご興味ある方は覗いてみてください👀 ↓↓

スクロールに応じてヘッダー画像サイズが変化する SwiftUI の実装 最小限の実装だと以下のような感じです。

import SwiftUI

struct RecipeMasterView: View {
    @State private var scrollOffset: CGFloat = 0
    let headerHeight: CGFloat = 590

    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                // ヘッダー画像部分
                GeometryReader { geometry in
                    HeaderView(offset: geometry.frame(in: .named("scroll")).minY)
                        .frame(width: geometry.size.width, height: max(0, headerHeight + geometry.frame(in: .named("scroll")).minY))
                        .offset(y: min(0, -geometry.frame(in: .named("scroll")).minY))
                }
                .frame(height: headerHeight)
   
                 // ・・・
                 // ここにリストやその他の要素
            }
       }
        .coordinateSpace(name: "scroll")
    }
}

struct HeaderView: View {
    let offset: CGFloat

    var body: some View {
        Image("eye-catch-image") // 任意の画像
            .resizable()
            .aspectRatio(contentMode: .fit)
            .offset(y: offset > 0 ? -offset : 0) // 上方向に移動
            .scaleEffect(offset > 0 ? (offset / 1000 + 1) : 1) // 拡大
    }
}

ヘッダーサイズは画像によって調整する感じになっており、柔軟さに欠ける実装部分です😭 (プロトタイプなのをいいことに若干サボってしまった。。。)

画像のアスペクト比に応じて高さを変えて、横幅一杯に表示や余白を持たせるなどが、調整として必要になってしまっています。

レシピ画面

レシピ画面はアプリの肝となる画面で、「完成品3Dオブジェクト」と「レシピ動画再生」, 「料理手順」の三つウィンドウが選択後に表示されるようになっています。

これによって、完成品のイメージを膨らませながら、レシピ動画を再生して料理を進めることができる想定です。

手順表示はレシピ動画の再生時間と連動しており、動画の再生位置に応じて手順のフォーカスが変更されるようにしました。

レシピ画面

ウィンドウを同時に開くのは visionOS では特に難しくなく、WindowGroup で指定した id でopenWindow を呼び出せば任意のウィンドウを開くことができます。(macOSで開発をされていた方であれば当然の内容かもしれませんね 👀)

ただ、動画のように真ん中に「レシピ動画再生」ウィンドウを表示して、その左右に「料理手順・完成品3Dオブジェクト」ウィンドウを表示する条件を満たしたい場合にはdefaultWindowPlacement で WindowPlacement を指定します。

developer.apple.com

今回の場合だと、レシピ動画再生ウィンドウ(videoPlayerWindow)に対して、料理手順(recipeWindow)・完成品3Dオブジェクト(volumeWindow)ウィンドウをそれぞれ .leading と.trailing で指定すると、openWindow で対象のウィンドウを開くと満たしたい条件で表示することができます。

以下のような実装になります。

@main
struct VisionCookingLabApp: App {
    // ・・・    
    var body: some Scene {
        // レシピ動画再生ウィンドウ
        WindowGroup(id: "videoPlayerWindow") {
            VideoPlayerWindow()
                .environment(state)
        }
        
        // 完成品3Dオブジェクトウィンドウ
        WindowGroup(id: "volumeWindow") {
            RecipeVolumeWindow()
                .environment(state)
        }
        .windowStyle(.volumetric)
        .defaultWindowPlacement { content, context in
            if let videoPlayerWindow = context.windows.first(where: { $0.id == "videoPlayerWindow" }) {
                // レシピ動画再生ウィンドウの左側に表示
                return WindowPlacement(.leading(videoPlayerWindow))
            } else {
                // 指定の Window が見つからなかった場合はユーザーの手元付近に表示
                return WindowPlacement(.utilityPanel)
            }
        }
        
        // 料理手順ウィンドウ
        WindowGroup(id: "recipeWindow") {
            RecipeWindow()
                .environment(state)
        }
        .defaultWindowPlacement { content, context in
            if let videoPlayerWindow = context.windows.first(where: { $0.id == "videoPlayerWindow" }) {
                // レシピ動画再生ウィンドウの右側に表示
                return WindowPlacement(.trailing(videoPlayerWindow))
            } else {
                // 指定の Window が見つからなかった場合はユーザーの手元付近に表示
                return WindowPlacement(.utilityPanel)
            }
        }
    }
}

スペシャルコンテンツ画面

最後、スペシャルコンテンツ画面です。ここでは空間写真や空間動画が楽しめるようになっています。

イマーシブ表示で楽しめるように QuickLook を利用した実装を選択しています。

スペシャルコンテンツ画面

実装時(2024/9)では AVFoundation などを利用して、イマーシブ表示で空間写真や空間ビデオを表示・再生ができなかったため QuickLook を採用しました。

developer.apple.com

実装は非常に簡単で、コンテンツの URL を指定して open メソッドを呼び出すと、QuickLook ウィンドウが立ち上がります。

タップして QuickLook ウィンドウを起動するなら以下のような実装です。シンプルですね。

import QuickLook
// タップ対象となるView
// ・・・ 
.onTapGesture {
    let previewItem = PreviewItem(url: contentURL, // 表示したいコンテンツのURL
                                  displayName: titleName, // 表示したいコンテンツのタイトル
                                  editingMode: .disabled)
    _ = PreviewApplication.open(items: [previewItem])
}

最後に

Apple Vision Pro を R&D として取り組んだ自身も iOS エンジニアで、VR や AR にしっかりと足を踏み入れた経験はありませんでした。

そのため、Apple Vision Pro の開発は手探りで、携われたのは非常に貴重な経験になりました。

R&D としてこれまでにない挑戦的な領域にも踏み込んでいけるのは、ヤプリの大きな魅力かと思います。

もちろん、ここで終わりではなく、Apple Vision Pro をもっと便利に楽しく使えるようなアイデアを募り、これからも開発を続けていければと思っています。

ここまでお読みいただきありがとうございました。



ヤプリにご興味がある方いましたら是非カジュアル面談でヤプリ社員と話しましょう!

open.talentio.com