Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Cluster_Extended Tokyo_WWDC 2023
Search
Cluster, Inc.
June 07, 2023
Technology
4k
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Cluster_Extended Tokyo_WWDC 2023
Cluster, Inc.
June 07, 2023
More Decks by Cluster, Inc.
See All by Cluster, Inc.
iOSDC2023_Cluster
clusterinc
1
2.8k
2023-05-24_Qiita_Night_AWS_vol.2_Cluster
clusterinc
0
580
gocon-2022-spring_sponsor-session.pdf
clusterinc
0
1.6k
cluster_SwiftUI
clusterinc
0
1.2k
cluster_Android_JetpackCompose
clusterinc
0
1.1k
AvatarMakerを支える技術 / cluster_avatarmaker_engineering
clusterinc
0
1.8k
clusterでLOD対応したときの話 / cluster LOD
clusterinc
0
2.1k
クラスター株式会社紹介資料 / Company Deck
clusterinc
5
340k
Other Decks in Technology
See All in Technology
Jev の中身は50行で再現できる。でも本物との差はどこにあるのか
hisuzuya
0
280
攻撃 3 時間 vs 復旧 10 時間。アクセスキー流出の非対称性を体感するために CTF を作った話
kazzpapa3
1
130
Technical Project Manager の役割とやりがい、少しだけ AI
yoshiiryo1
0
550
アクセラレーターで実現するServiceNow活用の高速化 / ServiceNow World Forum 2026
visional_engineering_and_design
0
120
Snowflake でできることが増えたら、 ABAC が必要になった
kaeton
0
130
AIエージェント時代、Kaggleにどう取り組むか
takanao
2
470
リアクションエンジニア ― 小さなリアクションから、話しかけやすい組織をつくる ―
ryuguyana
1
110
Autonomous AI Lakehouse - Live AI Hubから始めるセキュアなAIデータ活用 -
oracle4engineer
PRO
2
180
eBPF 簡介 2026 版
shunghsiyu
0
140
新米を「売り越さない」、Aurora DSQLで作る在庫ゼロ死守の限定ドロップEC
yama3133
0
140
「作れる」と「リリースできる」の間にあったもの── PdMを外して見えた、PdMがやるべき仕事/Buildable ≠ Shippable: What Removing the PM Taught Me About the PM’s Job
yappli_developers
0
420
kintoneで在庫管理システムを作るときの要件定義・設計ポイント
kai0707
0
200
Featured
See All Featured
The Curious Case for Waylosing
cassininazir
1
600
職位にかかわらず全員がリーダーシップを発揮するチーム作り / Building a team where everyone can demonstrate leadership regardless of position
madoxten
70
68k
Introduction to Domain-Driven Design and Collaborative software design
baasie
1
1.1k
Digital Projects Gone Horribly Wrong (And the UX Pros Who Still Save the Day) - Dean Schuster
uxyall
1
3.7k
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
500
SEOcharity - Dark patterns in SEO and UX: How to avoid them and build a more ethical web
sarafernandez
0
320
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Agile that works and the tools we love
rasmusluckow
331
22k
First, design no harm
axbom
PRO
2
1.4k
Groundhog Day: Seeking Process in Gaming for Health
codingconduct
0
450
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
11
1.1k
Transcript
メタバースプラットフォーム開発 におけるSwiftUIの活用とTips クラスター株式会社 / プラットフォーム事業本部 / ソフトウェアエンジニア Ahi To /
TAAT
Cluster, Inc. 2 概要 • メタバースプラットフォームでどのようにSwiftUIを活用して 開発を加速させているか • SwiftUIで苦戦したりUIKitと併用した部分のTips
Cluster, Inc. 3 自己紹介 Ahi To / TAAT クラスター株式会社 /
ソフトウェアエンジニア メタバースにチャレンジしてみたくて、2023年1 月からクラスターにジョイン! taatn0te @TAAT626 TAATHub
Cluster, Inc. 4 clusterとは? VRからスマホまで遊べるメタバースプラットフォーム
Cluster, Inc. 5 clusterとは? マルチプラットフォーム対応 iOS Android macOS Windows Desktop
/ VR Meta Quest 2 実はこれらのプラットフォーム向けのクライアントアプリケーションを毎週リ リースしている! PC,モバイル,VRに対応したマルチプラットフォームアプリのリリースフロー
Cluster, Inc. 6 アバターワーク チームのバーチャル仕事部屋 cluster内でMTGや雑談をしたり、その場で ワールドを 改 築 したり、リモートワークでもコ
ミュニケーションが取りやすい! クラスターに入社して 1ヶ月経ってみて アバターでWeb会議 Web会議もアバターで参加できる!アバター ワーク感があって良い!
7 メタバースプラットフォームでの iOSエンジニアの活躍領域
Cluster, Inc. 8 iOSエンジニアの活躍領域 outroom inroom バーチャル空間内の体験 バーチャル空間外 の非同期な体験
Cluster, Inc. 9 iOSエンジニアの活躍領域 outroom inroom cluster Unity-iPhone UnityFramework SwiftPackage
xcframework Unity as a LibraryをSwiftPM経由で導入してiOSビルド環境を改善した話 Unity as a Library Unityで構築したアプリケーションをネイティブアプリの ライブラリとして扱える
Cluster, Inc. 10 iOSエンジニアの活躍領域 iOSエンジニアは、バーチャル空間外の非同期的 な体験の機能開発を担当 outroom SwiftUIで苦戦したり、UIKitを併用する必要のあった部 分について紹介 clusterではiOS
15.0以上をサポートしており、新規画面 では積極的にSwiftUIを使用しているが、まだまだUIKit の部分が多く、SwiftUIだけではできない部分もあるの で、SwiftUIとUIKitを併用している
11 SwiftUIのTips
Cluster, Inc. 12 Tips SwiftUIのTabViewでインタラクティブなタブインジケーターを作る SwiftUIでImageをピンチイン・ピンチアウト・ダブルタップでズームさせる SwiftUIでTruncated Text ※本資料ではコードを抜粋しているため、詳細なコードは note記事やリポジトリを参照
Cluster, Inc. 13 Tips SwiftUIのTabViewでインタラクティブなタブインジケーターを作る SwiftUIでImageをピンチイン・ピンチアウト・ダブルタップでズームさせる SwiftUIでTruncated Text
Cluster, Inc. 14 SwiftUIのTabViewでインタラクティブなタブインジケーターを作る
Cluster, Inc. 15 TabViewでの基本的なページング struct ContentView: View { // タブの選択項目を保持する
@State var selection: Int = 0 var body: some View { TabView(selection: $selection) { Color.red.tag(0).ignoresSafeArea() Color.green.tag(1).ignoresSafeArea() Color.blue.tag(2).ignoresSafeArea() } .ignoresSafeArea() // PageTabViewStyleでページングできる // IndexDisplayMode.neverでインジケーター非表示 .tabViewStyle(.page(indexDisplayMode: .never)) } } PageTabViewStyleを指定すればページングできる
Cluster, Inc. 16 横スクロール時のオフセットを取得 TabView(selection: $selection) { Color.red .tag(0) .ignoresSafeArea(edges:
.bottom) .overlay { GeometryReader { proxy in Color.clear .onChange(of: proxy.frame(in: .global)) { newValue in // 表示中のタブをスワイプした時のみ処理する guard selection == 0 else { return } // 対象タブのスワイプ量を TabBarの比率に変換して、インジケーターの offsetを計算する let offset = -(newValue.minX - (screenWidth * CGFloat(selection))) / 3 // インジケーター位置を更新 indicatorPosition = offset } } } ... } proxyをグローバル座標に変換してインジケーター位置を更新 ※スクロール量をタブ数で割って比率を出す スクロール量を計測する Viewをオーバーレ イ
Cluster, Inc. 17 インタラクティブにインジケーター位置を更新 VStack(spacing: 0) { HStack { Text("Page1")
.frame(maxWidth: .infinity, maxHeight: .infinity) ... } .frame(height: 48) .overlay(alignment: .bottomLeading) { Rectangle() .foregroundColor(.black) .frame(width: geometry.size.width / 3, height: 4) .offset(x: indicatorPosition, y: 0) } TabView(selection: $selection) { Color.red .tag(0) .ignoresSafeArea(edges: .bottom) .overlay { ... } ... } .ignoresSafeArea(edges: .bottom) .tabViewStyle(.page(indexDisplayMode: .never)) } インジケーター位置を offsetで指定 インジケーター位置を更新
Cluster, Inc. 18 Tips SwiftUIのTabViewでインタラクティブなタブインジケーターを作る SwiftUIでImageをピンチイン・ピンチアウト・ダブルタップでズームさせる SwiftUIでTruncated Text
Cluster, Inc. 19 SwiftUIでImageをピンチイン・ピンチアウト・ダブルタップでズームさせる
Cluster, Inc. 20 SwiftUIでの基本的なズーム方法 struct ContentView: View { @State private
var currentScale: CGFloat = 1.0 @State private var lastMagnificationValue: CGFloat = 1.0 var body: some View { Image("sample") .resizable() .scaledToFit() .scaleEffect(currentScale) .gesture(MagnificationGesture().onChanged({ value in // 前回の拡大率に対する今回の拡大率の割合 let changeRate = value / lastMagnificationValue // 前回からの拡大率の変化分を考慮した現在のスケールを計算 currentScale *= changeRate // 最小・最大スケールの範囲内に収める currentScale = min(max(1.0, currentScale), 10.0) // 拡大率を保持 lastMagnificationValue = value }).onEnded({ value in // ジェスチャー開始時は 1.0から始まるため、ジェスチャー終了時に 1.0に戻す lastMagnificationValue = 1.0 })) } } MagnificationGestureで取得した拡大率を scaleEffectに反映させる ※取得できる拡大率はジェスチャー開始時を 1.0とした 値
Cluster, Inc. 21 拡大した画像をスクロールさせる struct ContentView: View { @State private
var aspectRatio: CGFloat = 1.0 @State private var currentScale: CGFloat = 1.0 @State private var lastMagnificationValue: CGFloat = 1.0 var body: some View { GeometryReader { proxy in ScrollView([.horizontal, .vertical], showsIndicators: false) { Image("sample") .resizable() .scaledToFit() .frame(width: proxy.size.width) // backgroundでGeometryReaderを使うことで、対象のViewのサイズを取得できる .background(GeometryReader { imageGeometry in Color.clear .onAppear { aspectRatio = imageGeometry.size.width / imageGeometry.size.height } }) .frame(width: proxy.size.width * currentScale, height: proxy.size.width / aspectRatio * currentScale, alignment: .center) .scaleEffect(currentScale) .gesture(magnification) } .background(.black) .ignoresSafeArea() } } } ScrollViewで囲ってframeを拡大率をもとに設 定すれば、拡大した画像をスクロールできる
Cluster, Inc. 22 ScrollView + MagnificationGestureの問題点 • MagnificationGestureの場合、ジェスチャー位置を取 得する方法が調べた限りではなさそうで、 scaleEffect(_:anchor:)のanchorに渡すことができない
ため、ジェスチャー位置を中心に拡大することができない • ジェスチャーとスクロールの相性が良くなく、ジェスチャーし ながらドラッグすると、ズームが中断されることがある
Cluster, Inc. 23 struct ContentView: View { var body: some
View { ImageViewer(imageName: "sample") .background(.black) .ignoresSafeArea() } } struct ImageViewer: UIViewRepresentable { let imageName: String func makeUIView(context: Context) -> UIImageViewer { let view = UIImageViewer(imageName: imageName) return view } func updateUIView(_ uiView: UIImageViewer, context: Context) {} } 改善策:SwiftUI + UIScrollView UIScrollView,UIImageViewを持つ
Cluster, Inc. 24 class UIImageViewer: UIView { private let imageName:
String private let scrollView: UIScrollView = UIScrollView() private let imageView: UIImageView = UIImageView() required init(imageName: String) { self.imageName = imageName super.init(frame: .zero) scrollView.delegate = self scrollView.maximumZoomScale = 10.0 scrollView.minimumZoomScale = 1.0 scrollView.showsHorizontalScrollIndicator = false scrollView.showsVerticalScrollIndicator = false scrollView.contentInsetAdjustmentBehavior = .never imageView.image = UIImage(named: imageName) imageView.contentMode = .scaleAspectFit imageView.isUserInteractionEnabled = true scrollView.addSubview(imageView) addSubview(scrollView) } 改善策:SwiftUI + UIScrollView public override func layoutSubviews() { super.layoutSubviews() scrollView.frame = bounds adjustImageViewSize() updateContentSize() updateContentInset() } } extension UIImageViewerView2: UIScrollViewDelegate { public func viewForZooming(in scrollView: UIScrollView) -> UIView? { return imageView } public func scrollViewDidZoom(_ scrollView: UIScrollView) { updateContentInset() } } UIScrollViewの上にUIImageViewを配置 ズームされるUIViewとして imageViewを返却
Cluster, Inc. 25 private func adjustImageViewSize() { guard let size
= imageView.image?.size else { return } let rate = min(scrollView.bounds.width / size.width, scrollView.bounds.height / size.height) // scrollView.boundsをもとに拡大率を計算して、 imageViewのサイズを調整する imageView.frame.size = CGSize(width: size.width * rate, height: size.height * rate) } private func updateContentSize() { // scrollView.contentSizeをimageViewのサイズに合わせる scrollView.contentSize = imageView.frame.size } private func updateContentInset() { // imageViewをscrollViewの中心に表示させる let edgeInsets = UIEdgeInsets( top: max((self.frame.height - imageView.frame.height) / 2, 0), left: max((self.frame.width - imageView.frame.width) / 2, 0), bottom: 0, right: 0) scrollView.contentInset = edgeInsets } 改善策:SwiftUI + UIScrollView
Cluster, Inc. 26 private var tapGestureRecognizer: UITapGestureRecognizer { let tapGestureRecognizer
= UITapGestureRecognizer() tapGestureRecognizer.numberOfTapsRequired = 2 tapGestureRecognizer .tapPublisher .sink { [weak self] recognizer in self?.onDoubleTap(recognizer: recognizer) } .store(in: &cancellables) return tapGestureRecognizer } private func onDoubleTap(recognizer: UITapGestureRecognizer) { let maximumZoomScale = scrollView.maximumZoomScale if maximumZoomScale != scrollView.zoomScale { let tapPoint = recognizer.location(in: imageView) let size = CGSize( width: scrollView.frame.size.width / maximumZoomScale, height: scrollView.frame.size.height / maximumZoomScale) let origin = CGPoint( x: tapPoint.x - size.width / 2, y: tapPoint.y - size.height / 2) scrollView.zoom(to: CGRect(origin: origin, size: size), animated: true) } else { scrollView.zoom(to: scrollView.frame, animated: true) } } UITapGestureRecognizerでダブルタップ ダブルタップ位置を中心に ズームする
Cluster, Inc. 27 Tips SwiftUIのTabViewでインタラクティブなタブインジケーターを作る SwiftUIでImageをピンチイン・ピンチアウト・ダブルタップでズームさせる SwiftUIでTruncated Text
Cluster, Inc. 28 SwiftUIでTruncated Text
Cluster, Inc. 29 SwiftUIでTruncated Text struct TruncatedTextSampleView: View { let
text = "吾輩は猫である。名前はまだ無い。どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャー ニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。 " var body: some View { VStack(spacing: 40) { Text(text) .lineLimit(3) TruncatedText(text, lineLimit: 3, ellipsis: .init(text: "More", color: .blue)) } } } テキストが表示できる行数を制限できるが、省略表示 を「...More」のようにカスタマイズできない 任意の省略文字やスタイルを指定できる
Cluster, Inc. 30 struct TruncatedText: View { ... var body:
some View { Group { Text(truncatedText) + Text(ellipsisPrefixText) + Text(ellipsisText) .font(ellipsisFont) .foregroundColor(ellipsis.color) } .multilineTextAlignment(.leading) .lineLimit(lineLimit) .lineSpacing(lineSpacing) .background( // lineLimitで制限されたテキストをレンダリングして、そのサイズを計測しながら表示できるテキストを更新する Text(text) // 計測用のレイヤーなので非表示にする .hidden() .lineLimit(lineLimit) .background(GeometryReader { visibleTextGeometry in Color.clear .onAppear { // 二分探索でテキストを省略しながら、NSAttributedStringを使って固定幅に対するテキストの高さを取得して、 // その高さがvisibleTextGeometry.size.height以下になったら終了 searchTruncatedText(proxy: visibleTextGeometry) } })) .font(Font(font)) } } SwiftUIでTruncated Text backgroundにサイズ計測用のレイヤーを非表示で用意して、二分探索 でテキストを省略しながら、表示領域に収まるまで省略テキストを更新 する ※このロジックはこちらの記事を元に実装
Cluster, Inc. 31 SwiftUIでTruncated Text // 二分探索でテキストを省略しながら、NSAttributedStringを使って固定幅に対するテキストの高さを取得して // その高さがvisibleTextGeometry.size.height以下になったら終了 private
func searchTruncatedText(proxy: GeometryProxy) { let size = CGSize(width: proxy.size.width, height: .greatestFiniteMagnitude) let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font] // 二分探索で省略テキストを更新する // 終了条件: mid == low && mid == high var low = 0 var heigh = truncatedText.count var mid = heigh while (heigh - low) > 1 { // 固定幅に対するテキストの高さを取得するためにNSAttributedStringを用いる let attributedText = NSAttributedString( string: truncatedText + ellipsisPrefixText + ellipsisText, attributes: attributes) let boundRect = attributedText.boundingRect( with: size, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil) if boundRect.size.height > proxy.size.height { truncated = true heigh = mid mid = (heigh + low) / 2 } else { if mid == text.count { break } else { low = mid mid = (low + heigh) / 2 } } truncatedText = String(text.prefix(mid)) } } NSAttributedStringで固定幅に対するテキストの高さを 取得して、表示領域の高さと比較しながら探索
Cluster, Inc. 32 Tips SwiftUIのTipsを紹介したが、いずれも力技が否めないので、 SwiftUIが今後さらに進化して、複雑なUIも宣言的で簡単に組め ることに期待!
None