見出し画像

SwiftUIでMotion Tab Barを再現する(100行)

Flutter用Motion Tab Bar Widgetの再現とApple SwiftUIチュートリアルを思い出す話。

端的にいうと

Flutter用Motion Tab Bar Widgetを100行で再現した際に起きた手戻りについての記事。iPhoneアプリ開発者、もしくはiPhoneアプリ開発に興味がある方向け。サンプルの動作にはXcode12が動作するMacが必要(macOS11.15以降必要)。

1) 発端

2020年のある日、TwitterのTimelineにFlutterで構築したカスタムTab Barが良いといった旨のTweet共にアニメーションがRetweetされているのを発見。

Tweet投稿にそれ以上の他意はないにしてもFlutter優位と思われてもSwiftUIを推す側としては、SwiftUIでも手軽に実現できること(しかも100行で)を証明するためカスタムTab BarをSwiftUIへ再現を試みることにした。

100行に収めるにあたって冗長なコードを圧縮したり、本来改行していた箇所を前行末に連結しているなど姑息なところはあるが100行にまとめている。

2) 再現UIの紹介 Motion Tab Bar - a beautiful animated widget

Flutter のWidget(UIパーツ)で紹介されていたカスタムTab BarのMotion Tab Barを対象とする(RetweetされていたTabBarは見つけることができなかった)。

Flutter Package: A Beautiful Animated Motion Tab Bar Widget - Flutter Resource

Widget作者もa beautiful animated widget と称しているのでアニメーションも得意なSwiftUI(アニメーションが得意なのはiOSネイティブアプリ全般だが)で再現するのはうってつけだろう。

オリジナルソースコードは見ずに再現した。。紹介ページのgifアニメーションだけを参考としているのでgifアニメーションで確認できないアニメーション速度などは相違がある。

またiOSのTabBarが対応している動的フォント(Dynamic Font)、アクセシビリティ、ダークモード、ローカライズ、横方向のレイアウトは対応を省いた。動きの再現を優先している。

3)再現したMotion Tab Bar

再現に使用したSwiftUI要素についてはBlogに記述している。

SwiftUI を使用したMotion Tab Barの再現 - SwiftUI100行チャレンジ③ | Irimasu Densan Planning - いります電算企画

画像1

ここでは再現したMotiuon Tab Barの確認手順について説明する。

Xcode12で新規プロジェクト(Command + Shit + P)をタップしAppウィザードを起動する。

iOS - App を選択しNextボタンをクリック。

画像4

プロジェクト設定は以下を参考。

・Product Nameは任意
・Interfacace にSwiftUI
・LangageにSwiftを選択
・Life Cycle は、UIKit App Delegate/SwiftUI Appいずれも選択可

画像5

Nextボタンを押してプロジェクトの保存位置を決定後Doneボタンをタップする。

新プロジェクト作成後XCodeがプロジェクトフォルダ内に出力したSwiftUIのソースコードcontent.swift内の、Content構造体を以下に差し替える。ソースコード末のContent_Preview構造体はPreview用に残しておく。

ここまでの手順でTab Barの動作を確認できる。SiwftUIのPreview機能、iPhone Simulator、実機でも動作確認できる。

4) 再現中の手戻りの話

SwiftUIをつかった実績としてはiOS13からSwiftUIを用いてUI構築している、MotionTabBarを再現する作業も手戻りなくできるだろう、と思っていたが、実際のところ手戻り箇所があった。

Stack機能はSwiftUIでの各種揃えを担当しZ軸、水平、垂直それぞれZStack、VStack、HStackが用意されている。ZStackを使うと追加した順にUIパーツの表示が反映されるため視覚的にUIパーツを追加する従来からあるiOSアプリ開発でのUI構築に近い形で作業を実施できると過信してTabBarのレイアウトを作成してしまった。

画像3

ZStackを多用した結果できたものは各種Stackが交互に使用された深いネストを持つTabBarが出来上がってしまった。深いネストを持つと見た目上は大した影響がないが内部的には以下の問題が発生している。

a)  機能しているか不明なStackが出てくる

b) アニメーションなど付加的要素を加える際に調整箇所が散在しやすい

従来からあるiOSアプリ開発でのUI構築では、全てのUIの基本パーツであるUIViewオブジェクトが子要素機能に対応し、子要素の追加順が視覚的にUIパーツの表示に反映される。UIViewオブジェクトの子要素機能をSwiftUIで実現するのにZStackを適用するのは用途として合致していない。

UIViewオブジェクトの子要素機能に相当するのはViewのoverlay、backgroundモデファイヤーである。

UIKitの子要素が表示内容のViewの中にViewの階層がつづく入れ子構造であったのに対して、overlay、backgroundそれぞれViewの前面、背面に配置されるViewを指定できる。overlayであればView前面に配置され、backgroundであれば背面に表示がまわりこむ。

他にもoverlay、backgroundには特性があってレイアウトに影響を与えない、hittest領域に含まれるなどがある。overlay、backgroundは1つのViewしか指定できないが、各種Stackも格納できるUIViewオブジェクトのような階層構造を実現したいのであればoverlayモデファイヤーとZStackを組み合わせる。

親View()
   .overlay(
       ZStack {
           子View1()
           子View2()
           子View3()
       }    
   )

TabBarでの応用として、Tabのアイテムにアイテムの例を示す。

Tabのアイテムにはアイコン画像と、ラベル存在し、これをVStackで実現する例を示すと、

VStack {
   Image(systemName: "house.fill")
       .font(.system(size: 24))
       .foregroundColor(.blue) 
   Text("Home")
       .font(.system(size: 9, weight: .bold))
       .foregroundColor(.black)	
}

となる。

上記の記述は縦方向の並びにVStackを使うのは適切に見えるが実際には、ImageやTextの高さはコンテンツによって高さが異なる。できればレイアウトが崩れる要素は排除したい。

どのようにレイアウト崩れ要素を排除するかというと、見えない矩形を導入し、overlay、backgroundモデファイヤーにそれぞれ、アイコン、ラベルを割り当てる。

Rectangle()
   .foregroundColor(.clear)
   .frame(width: 40, height: 40)
   .overlay(
       Image(systemName: "house.fill")
           .font(.system(size: 24))
           .foregroundColor(.blue) 
   )	
   .background(
       Text("Home")
           .font(.system(size: 9, weight: .bold))
           .foregroundColor(.black)	
           .offset(CGSize(width: 0, height: 32))
   )	

コード量は増えているが、レイアウトに影響するのは見えない矩形のサイズだけとなった。

同じ表現をZStackでも可能である。

ZStack {
   Image(systemName: "house.fill")
       .font(.system(size: 24))
       .foregroundColor(.blue) 
   Text("Home")
       .font(.system(size: 9, weight: .bold))
       .foregroundColor(.black)	
       .offset(CGSize(width: 0, height: 32))
}.frame(width: 40, height: 40)

簡潔だがZStackを排除したかったのでこちらは採用していない。

いずれにしろ、SwiftUIではUI構造の整理が可能となっている。

Viewのoverlay、backgroundモデファイヤーでの置き換えを含めTabBarの再現で検討しなおしたのは以下の2点である。

a) StackのうちViewの前面、背面(oberlay, background)に置き換え可能かを検討する

b) Stackが持つ揃え属性(alignment)を適用できるか兼用する。揃えと直行する方向への揃え属性を指定できる

考え方としてはUIKitの子要素の再現はStackの多用ではなく、overlay、backgroundモデファイヤーの適用を念頭におくと良い。

手直し前後で以下の図のような構造上の変化が起きている。Stackが2つ減っただけに見えるが手直し前ではアニメーション処理を加えると画面全体のレイアウトに影響を与えていた問題が回避されている。

画像3

5) AppleのSwiftUIチュートリアルを思い出す

よくよく思い出してみるとAppleのSwiftUIチュートリアルの開始早々Chapter 1 - SwiftUI Essentialsで既にStackが増えていく問題の回避策としてoverlay, backgroundが提示されている。チュートリアルを漫然と進めていたときには何につかうのかと思っていた。

Stackが多くなる問題に対してチュートリアルでは回避策となる手段は示されているも、いずれかのタイミングで示された手段を適用するかの判断について明示されているわけではない。

UI再現チャレンジのメリットはUIの見た目や挙動などのアセットが出揃った万全の状態から、作るだけというお膳立てがあるにもかかわらず手戻りが発生した。AppleとしてはSwiftUIチュートリアルで問題解決のための重要点を示しているが従来からあるiOSアプリ開発でのUI構築を前提としないチュートリアル内容だと思う。

SwiftUIの目玉機能であるStackに注目があつまりやすくStack多用すればレイアウトが完成できるような誤解があるかもしれない。SwiftUIでは見た目を維持しつつ構築方法を変化できる柔軟性を持ってるが、自在に扱うにはStackを含めた機能の組み合わせ方法を試行する必要がある。

SwiftUIの機能組み合わせの思考錯誤はあるていど短い期間で終わるのが適しているし今回のUI再現チャレンジのような練習が向いていると思う。なによりSwiftUIを使っていて楽しいと思える。

まとめ

・Flutter の美しいアニメーションMotion Tab Bar WidgetをSwiftUIで再現した。

・再現中に手戻りが発生したが回避方法をみつけて喜ぶもAppleのSwiftUIのチュートリアルで紹介済みでした。

この記事が気に入ったらサポートをしてみませんか?