visionOSで簡単なアプリを1つ作ろう
初めに
この記事は、フラー株式会社 Advent Calendar2023の17日目の記事となっています。
16日目はnosseさんによるフロントエンド未経験者が仕事をするためにやった1ヶ月の勉強でした!
本日は、12/17
日です。ちなみに1217
は199
番目の素数で199
も素数です、とてもいい数字ですね。
今年はApple
からvisionPro
の発表があり、僕にとっては最高の一年となりました。皆さんもそうですよね?
ちなみに発表からしばらく経ちましたが、皆さんは既にvisionOS
での開発に取り組んでいますでしょうか?
この記事は、visionOS
に興味はあるものの、まだ手をつけていない方を対象に、visionOS
でひとまず簡単なアプリを作ってみようという趣旨で作った記事となります。
是非最後までお楽しみいただければ幸いです!
今回の記事を全て見終わると作れるもの
今回の記事を全て見終わると、下記のような、少しだけリッチな天気予報アプリが自分で作れるようになります。visionOS
についてをゆるく学びつつ、このアプリを一緒に作っていきましょう!
visionOS
visionOS
でこれからアプリを作っていく前に、visionOS
に関して3つ重要な構成要素が含まれており、完全に理解する必要があります。
Windows
とVolumes
とSpaces
です。
Windows
visionOSアプリでは、1つまたは複数のウインドウを作成できます。ウインドウはSwiftUIで作成し、従来のビューやコントロールを含めることができます。3Dコンテンツを追加して体験に奥行きを与えることもできます。
Windows
を簡易的に説明すると、SwiftUI
での従来のView
を表示させることができる要素です。
Apple
の公式で公開されているHello Worldを参考にしながら説明すると、アプリ起動時表示されている長方形のものがWindows
であり、その中でView
を構築しています。
また、Windows
は可変可能で、.defaultSize
を使用することにより、サイズを変更できるようになります。
struct ExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.defaultSize(CGSize(width: 500, height: 500))
}
}
Windows
は、ユーザーの操作で可変可能となります。
これは、.windowResizability
に対して、default
でautomatic
が与えらているからです。
ユーザー操作での可変を制限するには、contentSize
、またはcontentMinSize
を使用することにより可変できる大きさを調節させます。
以上より、Windows
はView
を構成する中で重要な要素であり、内部でView
を作成することが出来、画面自体の大きさを可変することが出来る要素となります。
Volumes
3Dボリュームを使用してアプリに立体感を与えましょう。ボリュームとは、RealityKitやUnityを使用して3Dコンテンツを表示できるSwiftUIのシーンのことです。ボリュームは、共有スペースやアプリのフルスペースにおいて、どの角度からでも見ることができる体験を作り出します。
Volumes
を簡易的に説明すると、3Dコンテンツを表示することができる3D空間の箱です。Voluemes
を使用することにより、3DコンテンツをWindows
の中などに表示することが出来ます。
下記画像では、Windows
の中に、月の3Dモデルが表示されているかと思われます。この3DモデルがVolumes
となります。
Voluems
を表示させるには、Model3D
を使います。または、後述するRealityKit
を用いた実装で表示することもできます。
struct ExampleView: View {
var body: some View {
Model3D(named: "Example")
}
}
Model3D
は、URL
から直接コンテンツを表示させることもできます。
struct ExampleView: View {
var body: some View {
Model3D(url: URL(string: "https://example.com/robot.usdz")!)
}
}
以上より、Volumes
は3Dコンテンツを表示し、ユーザーに対して立体的なView
を提供する要素となります。
Spaces
デフォルトでは、アプリは共有スペースで起動されます。Macのデスクトップ表示のように複数のアプリが並んで配置されます。アプリはウインドウやボリュームを使用してコンテンツを表示し、ユーザーはそれらの要素を好きな場所に移動できます。よりイマーシブ感のある体験を提供するために、アプリ専用のフルスペースを開き、そのアプリのコンテンツだけを表示することもできます。フルスペースでは、ウインドウやボリュームを使用して自在に3Dコンテンツを作成し、別の世界へのポータルを出現させたり、ユーザーに環境へのフルイマーシブな体験を提供したりできます。
Spaces
を簡易的に説明すると、フルイマーシブな体験を提供する要素です。
Hello World
アプリで、The Solar System
のView Outer Space
を押下すると下記のような画面が表示されます。
そしてこの空間を表現しているのがSpaces
となります。
Spaces
の実装に関しては、ImmersiveSpace
で表現する形となっており、本章の実践で紹介しますのでお楽しみにしていてください、
以上より、Spaces
はユーザーに対して没入感のある画像や動画を提供する要素となります。
RealityKit
RealityKit は、visionOS アプリの作成や、iOS、macOS、tvOS 用の拡張現実 (AR) アプリの作成に使用できる、高性能 3D シミュレーションおよびレンダリング機能を提供します。RealityKit は、ARKitを利用して仮想オブジェクトを現実世界にシームレスに統合するAR ファースト 3D フレームワークです。
RealityKit
は、記述されている通り、visionOS
やARアプリの開発を手助けしてくれるフレームワークです。今回ImmersiveSpace
を実装する際に使用します。
RealityKit
の重要な概念として、Entity
と呼ばれるものがあります。
Entity
は3D空間内のオブジェクトや要素を表す基本的な構成要素です。
Entity
は視覚的な表現や動作を持つことができ、例えば、3Dモデル、ライト、カメラ、アニメーションなどがEntity
として表現されます。
RealityKit
は、visionOS
と合わせて高度なグラフィック体験を使用する際に必ず使う要素なので、まだ使用したことがない方は下記を一度参照してみてください。
実践
それでは、冒頭で説明した天気予報アプリを作っていきましょう。
プロジェクト作成
-
初めに、
Xcode
を開きNew Project
で新しいプロジェクトを作成してください。 -
次に、
Choose a template for your new project
でvisionOS
のApp
を選択し、Next
を選択してください。
この画面でvisionOS
のApp
が存在しない方は、visionOS
のSDK
をダウンロードしていない可能性がありますので、公式サイトからダウンロードしてください。
https://developer.apple.com/jp/visionos/
また、Xcode
のバージョンが対応していない可能性もありますので、Xcode15 beta2
以上を使用してください。
-
Initial Scene
でWindow
とScene
から選択できます。今回はdefault
のWindow
を選択しましょう。
-
Immersive Space Renderer
で、None
、RealityKit
、Metal
から選択できます。今回はdefault
のNone
を選択して、Next
を押下してください。
以上で新規プロジェクト作成は完成です。
visionOSアプリの実装
それではvisionOS
アプリの実装に入っていきます。
今回、実装の説明を設けて記述してありますが、visionOS
関連のことだけ説明し、SwiftUI
や他技術のことに関しては省略しておりますので悪しからず。
Spacesの実装
プロジェクトの作成でアプリを作成したので、現在は、App.swif
とContentView.swift
がアプリ内に存在するかと思います。
それでは、今回天気予報のアプリを作成するため、天気のモデルを作成しましょう。
Weather.swift
ファイルを作成し、下記を追加してください。完了したら、Assets.xcassets
内でのImage set
で下記モデル名の画像を作成してください。
enum Weather: String {
case rain
case snow
case sunny
}
次に、ImmersiveView.swift
というファイルを作成し、下記を追加してください。
import Combine
import SwiftUI
import RealityKit
struct ImmersiveView: View {
// MARK: - Properties
@Binding
private(set) var weatherName: Weather
@State
var cancellable: AnyCancellable?
// MARK: - View
var body: some View {
RealityView { content in
let entity = Entity()
cancellable = TextureResource.loadAsync(named: weatherName.rawValue).sink { error in
print(error)
} receiveValue: { resource in
var material = UnlitMaterial()
material.color = .init(texture: .init(resource))
entity.components.set(ModelComponent(
mesh: .generateSphere(radius: 1000),
materials: [material]
))
entity.scale *= .init(x: -1, y: 1, z: 1)
}
content.add(entity)
}
}
}
実装の内容について説明です。
RealityView
は、XR
コンテンツを描画するための親コンテナとなります。
RealityView { content in }
RealityKit
の章で説明した通り、3D
オブジェクトを表現するためにEntity
を作成し、TextureResource.loadAsync
で非同期のテクスチャを読み込むためのCombine
操作を開始します。
成功した際に、テクスチャを使用し、Entity
に適応させます。今回は、球体(UnlitMaterial)
を生成し、その表面にテクスチャを適用させています。
let entity = Entity()
cancellable = TextureResource.loadAsync(named: weatherName.rawValue).sink { error in
print(error)
} receiveValue: { resource in
var material = UnlitMaterial()
material.color = .init(texture: .init(resource))
entity.components.set(ModelComponent(
mesh: .generateSphere(radius: 1000),
materials: [material]
))
entity.scale *= .init(x: -1, y: 1, z: 1)
}
最後に、作成したエンティティをXRコンテンツにadd
します。
content.add(entity)
次にApp.swift
を下記のように修正してください。
import SwiftUI
@main
struct ParticleWeatherApp: App {
// MARK: - Properties
@State
var weatherName: Weather = .snow
@Environment(\.openImmersiveSpace)
var openImmersiveSpace
@Environment(\.dismissImmersiveSpace)
var dismissImmersiveSpace
// MARK: - View
var body: some Scene {
WindowGroup {
ContentView()
.task {
await openImmersiveSpace(id: "ImmersiveSpace")
}
}
.defaultSize(width: 1.5, height: 0.5, depth: 0, in: .meters)
ImmersiveSpace(id: "ImmersiveSpace") {
ImmersiveView(weatherName: $weatherName)
}
.immersionStyle(selection: .constant(.full), in: .full)
}
}
実装の内容について説明です。
今回定義しているプロパティに関して、環境変数のopenImmersiveSpace
とdismissImmersiveSpace
を定義しています。
この2点は、ImmersiveSpace
を開閉するための処理を提供している重要な概念です。
@Environment(\.openImmersiveSpace)
var openImmersiveSpace
@Environment(\.dismissImmersiveSpace)
var dismissImmersiveSpace
ContentView
が初めて表示された際に実行される非同期タスクの.task
でawait openImmersiveSpace(id: "ImmersiveSpace")
を実行し、ImmersiveSpace
を表示させるようにしています。
そして、.defaultSize(width: 1.5, height: 0.5, depth: 0, in: .meters)
を使用し、初回表示するWindows
の大きさを設定しています。
WindowGroup {
ContentView()
.task {
await openImmersiveSpace(id: "ImmersiveSpace")
}
}
.defaultSize(width: 1.5, height: 0.5, depth: 0, in: .meters)
ImmersiveSpace(id: "ImmersiveSpace")
を宣言し、今回表示させるImmersiveView
のコンテンツを表示させています。
そして、.immersionStyle(selection: .constant(.full), in: .full)
で、コンテンツのスタイルを.full
に設定しています。
ImmersiveSpace(id: "ImmersiveSpace") {
ImmersiveView(weatherName: $weatherName)
}
.immersionStyle(selection: .constant(.full), in: .full)
現状のもので一度Build
してみてください。下記のような画面が表示されていると思います!
これでSpaces
の実装が一通り完成しました。
Windowsの実装
続いて、天気予報のView
を表現するため、WeatherForecast.swift
を作成し、天気予報のモデルを作成してください。
import Foundation
struct WeatherForecast {
// MARK: - Properties
var id = UUID().uuidString
let time: String
let temperature: String
let weather: Weather
}
WeatherForecast.swift
を作成したら、いよいよContentView
の修正です。
下記のようにContentView
を修正してください。
import RealityKit
import RealityKitContent
import SpriteKit
import SwiftUI
struct ContentView: View {
// MARK: - Properties
@Binding
private(set) var weather: Weather
private let weatherForecasts: [WeatherForecast] = ["{データを適当に複数追加}"]
// MARK: - View
var body: some View {
GeometryReader { geo in
ZStack {
if let weatherParticle = weatherParticle(size: geo.size, weather: weather) {
SpriteView(scene: weatherParticle)
}
VStack {
weatherInformation()
forecastScrollView()
}
.padding()
.position(x: geo.size.width / 2, y: geo.size.height / 2)
}
}
}
private func weatherInformation() -> some View {
HStack {
VStack {
Text("12月14日(木)")
.font(.system(size: 60))
}
.padding()
VStack {
Text("現在地")
Text("10°")
HStack {
Text("最\n高")
Text("10°")
Text("最\n低")
Text("2°")
}
.font(.system(size: 25))
Text(weather.rawValue)
.font(.system(size: 35))
}
.padding()
}
.shadow(radius: 5)
.font(.system(size: 50))
.bold()
}
private func forecastScrollView() -> some View {
ScrollView(.horizontal) {
HStack {
ForEach(weatherForecasts, id: \.id) { weatherForecast in
Button(action: {
weather = weatherForecast.weather
}, label: {
weatherCell(weatherForecast: weatherForecast)
})
.padding()
}
}
}
}
private func weatherCell(weatherForecast: WeatherForecast) -> some View {
VStack {
Text(weatherForecast.time)
.font(.system(size: 25))
Model3D(named: modelFileName(for: weatherForecast.weather)) { model in
model
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.padding()
} placeholder: {
ProgressView()
}
Text(weatherForecast.temperature)
.font(.system(size: 25))
}
.shadow(radius: 5)
.bold()
.frame(width: 200, height: 200)
}
private func modelFileName(for weather: Weather) -> String {
return weather == .sunny ? "Sun" : weather == .rain ? "Rain" : "Snow"
}
private func weatherParticle(size: CGSize, weather: Weather) -> SKScene? {
guard weather != .sunny else { return nil }
let fileName: String = weather == .rain ? "RainParticle" : "SnowParticle"
let backgroundNode = SKSpriteNode(imageNamed: weather.rawValue)
backgroundNode.position = CGPoint(x: size.width / 2, y: size.height / 2)
let emitterNode = SKEmitterNode(fileNamed: fileName)!
let scene = SKScene(size: size)
scene.addChild(backgroundNode)
scene.addChild(emitterNode)
scene.anchorPoint = .init(x: 0.5, y: 1)
return scene
}
}
今回、Rain
、Snow
、Sun
の3つの3Dモデルをアプリで使用しています。
3Dモデルに関しては、Reality Composer Pro
で作成してください。
それか、作成に手をつけたくないとのことでしたら、sketchfab
というサイトで無料のusdz
の3Dモデルをダウンロードできるので、各々で用意してください。
Particle
についてですが、今回、SnowParticle
、RainParticle
を使用しています。
New File
から2つのParticle
を作成してください。
それでは、実装の内容について説明です。
Model3D
はVolumes
章で説明した通り、3Dモデルを非同期的に表示するView
です。
今回はinit(named:bundle:content:placeholder:)
を使用し、読み込み時に、ProgressView()
を表示させるように実装しています。
また、クロージャーで戻り値model
を宣言し、サイズやアスペクト比率を調節しています。
Model3D(named: modelFileName(for: weatherForecast.weather)) { model in
model
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.padding()
} placeholder: {
ProgressView()
}
weatherParticle(_:)
で、View
の背景を季節に合わせたものに設定するようにしており、Model3D
の引数にmodelFileName
を経由し、表現するようにしています。
private func weatherParticle(size: CGSize, weather: Weather) -> SKScene? {}
Model3D(named: modelFileName(for: weatherForecast.weather))
最後にweatherName
が変わったときにImmersiveSpace
を変更したいので、App.swift
を下記のように修正してください。
import SwiftUI
@main
struct ParticleWeatherApp: App {
// MARK: - Properties
@State
var weatherName: Weather = .snow
@Environment(\.openImmersiveSpace)
var openImmersiveSpace
@Environment(\.dismissImmersiveSpace)
var dismissImmersiveSpace
// MARK: - View
var body: some Scene {
WindowGroup {
ContentView(weather: $weatherName)
.task {
await openImmersiveSpace(id: "ImmersiveSpace")
}
.onChange(of: weatherName) { _, newValue in
Task {
await dismissImmersiveSpace()
await openImmersiveSpace(id: "ImmersiveSpace")
}
}
}
.defaultSize(width: 1.5, height: 0.5, depth: 0, in: .meters)
ImmersiveSpace(id: "ImmersiveSpace") {
ImmersiveView(weatherName: $weatherName)
}
.immersionStyle(selection: .constant(.full), in: .full)
}
}
以上で冒頭で説明した通りの少しだけリッチな天気予報アプリが完成しているかと思います!
いかがでしただしょうか?
簡単にvisionOS
でアプリを1つ作れたのではないでしょうか!?!?
終わりに
今回作成したもので、ImmersiveSpace
の背景を変更する際に、一度閉じて再度開く処理をしていると思います。現状(2023/12/17)、おそらくSpaces
を簡易的に更新する処理が追加されていないので不細工な処理になっています。Apple
さん、対応して!
ちなみに弊社CTO
が、会社用にvisionPro
を1台は必ず買うと言われていたので、僕は楽しみにしつつ当記事を書いてました。
購入された会社のvisionPro
を使うのが最近の僕の夢ですので、楽しみしてますね弊社!!!!!!!
そんな素敵な会社を宣伝しておきます。
ということで、今回のvisionOS
で簡単なアプリを1つ作ろうの記事を終わりとさせていただきます。
次回はcanacelさんのPointFreeのサブスクに登録してみた話です!
Discussion