Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Cluster_Extended Tokyo_WWDC 2023
Search
Cluster, Inc.
June 07, 2023
Technology
3.8k
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.7k
2023-05-24_Qiita_Night_AWS_vol.2_Cluster
clusterinc
0
530
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.7k
clusterでLOD対応したときの話 / cluster LOD
clusterinc
0
2k
クラスター株式会社紹介資料 / Company Deck
clusterinc
5
340k
Other Decks in Technology
See All in Technology
Disciplined Vibes: Scaling AI-Assisted Engineering
sheharyar
0
120
AmazonRoute 53ではじめてのドメイン取得!HTTPS化までの道のりを整理してみた
usanchuu
3
120
白金鉱業Meetup_Vol.24_「AIエージェントは分けるほど良い」は本当か? / Is it true that “the more you divide AI agents, the better”?
brainpadpr
1
190
Oracle Cloud Infrastructure IaaS 新機能アップデート 2026/3 - 2026/5
oracle4engineer
PRO
1
250
脆弱性対応、どこで線を引くか
rymiyamoto
0
340
実装は速くなった、レビューはどうする? ― 自身のレビューをAIで再現させるサーヴァントエンジニアリングのすゝめ / Implementation got faster. So what about reviews? — An invitation to Servant Engineering: Recreating your own code reviews with AI
nrslib
8
4.5k
社内 AI エージェント Synapse と セマンティックレイヤーの育て方
hiroakis
2
1.5k
中期計画、2回作ってみた ~業務委託と正社員、両方の視点から~
demaecan
1
620
地球に⽣きるAI —GeoAIと「中間領域」— / AI Living on Earth — GeoAI and the “Intermediate Layer” —
ykiyota
0
210
"何を作るか"を任される エンジニアは、どう育つのか
yutaokafuji
1
540
新しいVibe Codingと”自走”について
watany
5
280
Snowflakeと仲良くなる第一歩
coco_se
4
380
Featured
See All Featured
Bridging the Design Gap: How Collaborative Modelling removes blockers to flow between stakeholders and teams @FastFlow conf
baasie
0
580
Google's AI Overviews - The New Search
badams
0
1k
HDC tutorial
michielstock
2
700
Facilitating Awesome Meetings
lara
57
7k
Fashionably flexible responsive web design (full day workshop)
malarkey
408
66k
SEO for Brand Visibility & Recognition
aleyda
0
4.6k
Context Engineering - Making Every Token Count
addyosmani
9
950
AI Search: Implications for SEO and How to Move Forward - #ShenzhenSEOConference
aleyda
1
1.3k
Ten Tips & Tricks for a 🌱 transition
stuffmc
0
130
ラッコキーワード サービス紹介資料
rakko
1
3.6M
Balancing Empowerment & Direction
lara
6
1.2k
Digital Projects Gone Horribly Wrong (And the UX Pros Who Still Save the Day) - Dean Schuster
uxyall
0
1.7k
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