2021年9月からマッハバイトに業務委託として参画している草間と申します。
iOSエンジニアとして参画し、今ではandroid・Web側も担当するようになり楽しく業務しています。
前回は導入した話をしました。
今回は、SwiftUIの導入で苦労した話をします。
OSSの動きが想定と違った問題
アニメーションのViewが画面サイズより大きくなる
マッハバイトiOSアプリでは、アニメーションの実装にLottieを利用しています。
SwiftUIからLottieのViewを利用できるように次の実装しました。
import Lottie import SwiftUI struct LottieView: UIViewRepresentable { private let lottieView = LottieAnimationView() func makeUIView(context: Context) -> LottieAnimationView { lottieView } func updateUIView(_ uiView: LottieAnimationView, context: Context) { .... } }
前回話したUIViewをSwiftUIで利用するUIViewRepresentable
を利用してLottieのViewをそのまま返す実装です。
この実装の場合、LottieのViewが画面サイズに収まらずだいたい倍くらいの大きさで表現されてしまいます。
そこでLottieのViewをそのまま返さずサイズのベースになるUIViewを利用する形に変えました。
import Lottie import SwiftUI import UIKit struct LottieView: UIViewRepresentable { private let lottieView = LottieAnimationView() private let baseView = UIView() init(animationResourceName: String) { lottieView.animation = LottieAnimation.named(animationResourceName) lottieView.contentMode = .scaleAspectFit baseView.addSubview(lottieView) lottieView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ baseView.topAnchor.constraint(equalTo: lottieView.topAnchor), baseView.leadingAnchor.constraint(equalTo: lottieView.leadingAnchor), baseView.trailingAnchor.constraint(equalTo: lottieView.trailingAnchor), baseView.bottomAnchor.constraint(equalTo: lottieView.bottomAnchor) ]) lottieView.updateConstraints() } func makeUIView(context: Context) -> UIView { baseView } func updateUIView(_ uiView: UIView, context: Context) { } }
この実装でSwiftUI上でUIViewの大きさが画面サイズまでで制限され、LottieのViewはUIViewの大きさに制約がかかるため、画面サイズを超えないようにできました。
画面サイズの違いで発生した問題
scaledToFit
だけでは画像サイズがよしなに広がってくれない?
前回、マッハバイトiOSアプリの静的な画面をSwiftUIに置き換えた話をしました。
置き換えた画面には上部にヘッダー画像があります。
実装当時の理解では、Image
オブジェクトは上下左右にpaddingやmarge等入れなければ
親のViewに対していい感じに広がり、scaledToFit
でよしなに表示してくれると思っていました。 (下の画像のように)
ところが実行してみると次のコードでは下の画像のようになりました。
Image("TestTop")
.scaledToFit()
見事に左右に隙間が発生しました。
画像をベクターで用意していなかったこともあると思いますが、画像をImageオブジェクトに合わせてリサイズする必要がありました。
Image("TestTop") .resizable() .frame(maxWidth: .infinity) .scaledToFit()
上記のように変更することで思っていたように表現できました。
コードとしては.resizable()
でリサイズされるようにし、.frame(maxWidth: .infinity)
で横幅を親オブジェクトの横幅いっぱいに広がるように設定しています。
実機でしか発生しなかった問題
文字が省略されてる!?
画像サイズの違いで発生した問題が解決すると次はちゃんと全文表示されていたはずのテキストが省略表示になっていました。
画像の対応を入れる前は全文表示されていたはずなのに。。。と汗をかくくらい焦りました。
実装時はどうして省略されてしまっているのかを調べず対策方法を探しました。
対策としては.fixedSize(horizontal: false, vertical: true)
を実装することでした。
Text("てすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすと") .foregroundColor(.white) .background(Color.gray) .fixedSize(horizontal: false, vertical: true) .modifier(BorderTextModifier())
こうすることで次の画像のように全文表示されるようになります。
検証したこと
SwiftUIに置き換えたアプリをリリースした後になぜ問題が発生したのかを軽く検証してみました。
検証する上でXIBからSwiftUIを利用しているから発生したのではないかと仮説を立てました。
検証環境としてプロジェクトを3つ用意
仮説を検証するため次の3つのプロジェクトを作成しました。
- SwiftUIオンリーのプロジェクト
- StoryboardからSwiftUIを表示するプロジェクト
- XIBからSwiftUIを表示するプロジェクト
結論からいうとXIBからSwiftUIを利用しているから発生したわけではなかったです。
文字が省略されてしまう条件は?
サンプルプロジェクトで次のように実装して検証してみました。
struct SampleSwiftUI: View { var body: some View { ZStack { Color.gray .ignoresSafeArea() ScrollView { VStack { Text("てすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすと") .foregroundColor(.white) .background(Color.gray) .modifier(BorderTextModifier()) VStack(alignment: .trailing, spacing: 10) { Text("てすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすと") .foregroundColor(.white) .background(Color.gray) .modifier(BorderTextModifier()) Text("てすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすと") .foregroundColor(.white) .background(Color.gray) .modifier(BorderTextModifier()) Text("てすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすと") .foregroundColor(.white) .background(Color.gray) .modifier(BorderTextModifier()) Text("てすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすと") .foregroundColor(.white) .background(Color.gray) .modifier(BorderTextModifier()) Text("てすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすと") .foregroundColor(.white) .background(Color.gray) .modifier(BorderTextModifier()) }.padding(.horizontal, 16) Text("てすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすと") .foregroundColor(.white) .background(Color.gray) .modifier(BorderTextModifier()) } } } } private struct BorderTextModifier: ViewModifier { func body(content: Content) -> some View { content .frame(maxWidth: .infinity, alignment: .leading) .font(.system(size: 12)) .padding(.all, 16) .lineSpacing(4) .overlay( RoundedRectangle(cornerRadius: 6) .stroke(.black, lineWidth: 1) ) } } } struct SampleSwiftUI_Previews: PreviewProvider { static var previews: some View { SampleSwiftUI() } }
ViewModifierについてはAppendixで話します。
この場合では文字列は省略されず全文表示されていました。
文字列のみの場合は省略されないことがわかったので実実装同様に上部にヘッダー画像を入れることにしました。
ScrollView { VStack { Image("TestTop") .... }
上記のように画像だけ入れてみました。
この場合でも文字列は省略されず全文表示されていました。
ここまでくるとおわかりいただけると思いますが、.resizable()
をした結果省略されることがわかりました。
ただし、.resizable()
だけで省略されるわけではありません。
上のコードにImage
を追加して確認しました。
ScrollView { VStack { // これを追加 Image("TestTop") .resizable() .frame(maxWidth: .infinity) .scaledToFit()
すると一部だけ省略される現象が発生しました。
コードとしては次の部分です。
struct SampleSwiftUI: View { var body: some View { ZStack { Color.gray .ignoresSafeArea() ScrollView { VStack { Image("TestTop") .resizable() .frame(maxWidth: .infinity) .scaledToFit() VStack(alignment: .trailing, spacing: 10) { // ここのText部分 Text("てすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすとてすと") .foregroundColor(.white) .background(Color.gray) .modifier(BorderTextModifier())
Image
と同じ階層に実装しているText
では省略が発生せず、VStack
の中に入れて階層を変えたText
で発生することがわかりました。
実装を見ていただけるとわかりますが、省略が発生するVStack
内にはText
が複数実装してあります。
では、VStack
に1個のみText
を実装した場合はどうなるかというと、省略されることはありません。
次に気になったのは何個までなら省略されないのかでした。
結論としては、VStack
内に2つまでは省略されないです。
※画像に関しては僕の知識不足なところが否めないので割愛します。
※Lottieについてはサンプルの用意が難しかったため検証していません。
まとめ
検証した結果次のことがわかりました。
Image
のリサイズと同階層のVStack
内のText
は省略される場合がある
今回検証したプロジェクトはGitHubにあげています。
ここで紹介したコードにHStack
を加えてみたりしています。
まだまだ検証パターンが足りないかもしれないので、別のパターンも試してみたいときなどにご活用いただければ幸いです。
最後に
iOSエンジニアの方ならいつものことだと思いますが、シミュレーターではきれいに表示されます。
昔からこういうことは多発していたのでちゃんと実機でも確認しましょうというのが僕の今回の反省事項でした。
少しでも誰かの苦労解決の役にたてば幸いです。
Appendix
検証したことで紹介したコードで前回話をしていないものがあったのでこちらで話をします。
ViewModifier
ViewModifierというプロトコルを継承した構造体を利用してText
の属性を制御しています。
このViewModifier
を簡単に説明するならCSSのようなものというのがイメージしやすいかなと思います。
private struct BorderTextModifier: ViewModifier { func body(content: Content) -> some View { content .frame(maxWidth: .infinity, alignment: .leading) .font(.system(size: 12)) .padding(.all, 16) .lineSpacing(4) .overlay( RoundedRectangle(cornerRadius: 6) .stroke(.black, lineWidth: 1) ) } }
上記のように実装します。
この構造体を次のように使うと設定している属性が反映されます。
Text("テスト")
.modifier(BorderTextModifier())
利点は何度も同じものを記述しなくて済むところにあります。