43
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ZOZOAdvent Calendar 2022

Day 18

Apple MusicのUIをSwiftUIで実装してみた際のメモ

Last updated at Posted at 2022-12-17

この記事は ZOZO Advent Calendar 2022 カレンダーVol.6の18日目の記事です。

はじめに

今年のWWDCでもSwiftUIのアップデートがあり、SwiftUIは年々使いやすくなっている印象です。
SwiftUIが発表されてから3年ぐらい経ちますが、ちょっとしたサンプル実装ぐらいでしか触れてきませんでした。
他社の採用事例も増えてきており、流石に無視出来なくなってきているのでとてもマイペースではありますが、Apple MusicのUIをSwiftUIで実装してみています。


実装はまだ一部ですが、個人的なメモを残しているのでそれを公開したいと思います。

環境

Xcode 14.1
iOS 16+

メモ

modifierの順序は大事

Viewはstructだから。
例えばボーダー込みで角丸にしたい時。border → cornerRadiusの順序。

Rectangle()
    .aspectRatio(1, contentMode: .fill)
    .foregroundColor(.pink)
    .border(.gray, width: 0.5)
    .cornerRadius(8)

逆だとborderが角丸にならない。

Rectangle()
    .aspectRatio(1, contentMode: .fill)
    .foregroundColor(.pink)
    .cornerRadius(8)
    .border(.gray, width: 0.5)

GeometryReader

コンテンツの位置とサイズを取得できるので、端末サイズの比率でViewのサイズを指定したいときに使える。

GeometryReader { proxy in
    LazyVStack(spacing: 0) {
        ...
    }
    .frame(width: proxy.size.width * 0.7)
}

ScrollViewのcontentInsets指定

配下のViewにpaddingを指定する。

ScrollView(.horizontal, showsIndicators: false) {
    LazyHStack(spacing: 12) {
		    ...
		}
		.padding(.horizontal, 20)
}

@ViewBuilder 属性を使ってViewを返す

条件によって返すViewを変えたい時などに使える。

@ViewBuilder
private func section(with section: State.Section, screenWidth: CGFloat) -> some View {
    switch section {
    case let .topPicks(state):
        MediumPortraitRectangleHGridSection(
            state: state,
            frameWidth: screenWidth,
            paddingHorizontal: paddingHorizontal
        )

トップレベル(View階層の)のVStackのspacingは0にするのが良さそう

階層が深くなると思わぬところにspacingが効いてしまうことがあった。

var body: some View {
    VStack(spacing: 0) {
        HStack(alignment: .center, spacing: 20) {
            VStack(alignment: .leading, spacing: 4) {
                HStack(spacing: 2) {
                    Image(systemName: "applelogo")
                        Text(state.headline)
                            .bold()
                        }
                        Text(state.subheadline)
                            .foregroundColor(.gray)
                    }

想定通り組めるなら余計なSpacerやpaddingが減るので使ったほうがいい。

Viewを重ねるときはZStackを使う

ZStack(alignment: .leading) {
    Rectangle()
        .background(Color.black)
        .opacity(0.2)
    
    HStack(alignment: .center, spacing: 14) {
        VStack(alignment: .leading, spacing: 4) {
            Group {
                HStack(spacing: 0) {
                    Text("LIVE")
                    Text("・")
                    Text(state.timetable)
                }
                .font(.caption)
                
                Text(state.title)
                    .lineLimit(1)
                    .bold()
                
                Text(state.caption)
                    .lineLimit(nil)
                    .layoutPriority(1)
            }
            .font(.system(size: 15))
            .foregroundColor(.white)
        }
        Spacer()
        
        Image(systemName: "play.circle.fill")
            .resizable()
            .scaledToFit()
            .foregroundColor(.white)
            .frame(width: 40)
            .opacity(0.8)
    }
    .padding(.horizontal, 20)
}

異なるViewに同じmodifierを適応させたい場合はGroupが使える

Group {
	Text("1")
	Text("2")
}
.foregroundColor(.white)
.multilineTextAlignment(.center)

レイアウトを優先させたいViewにはlayoutPriorityを使う

例えばテキストが長くなった時、上のテキストを優先させたいなど。

Text("長い文字列")
	.layoutPriority(1)
Text("長い文字列")

テキストが短いときは上詰め、長いときは一杯にしたいとき Spacer(minLength: 0) を使う

VStack(alignment: .leading) {
	Text("長い文字列")
	// minLength指定がないとSpacerの分、文字が広がらない
	Spacer(minLength: 0)
}

テキストの複数行は .lineLimit(nil) を使う

Text(state.caption)
    .lineLimit(nil)

ライトグレーを使う

// UIColorを使う
Color(uiColor: .lightGray)
// systemGrayを使う
Color(.systemGray6)

ForEachでoffsetを使用する方法

ForEach(Array(array.enumerated()), id: \.element) { offset, element in

previewDevice

プレビューの端末指定ができる。(複数可)

struct BrowseView_Previews: PreviewProvider {
    static var previews: some View {
        BrowseView()
            .previewDevice(PreviewDevice(rawValue: "iPhone 14 Pro"))
            .previewDisplayName("iPhone 14 Pro")
    }
}

次のコマンドをターミナルで実行すると使用できる端末一覧が取得できる

xcrun simctl list devicetypes

LazyVStack内でListは使えない?

深ぼってないので深ぼる。

ScrollViewでページングができない

UIScrollViewに存在した isPagingEnabled と同等のものがない

Xcode 14からSFSymbolが検索できるようになって便利

スクリーンショット 2022-12-16 18.06.20.png

所感

まだベストプラクティスがわからずこれでいいのかな?と悩むことも多い。大体VStack, HStackで組めてしまうので他のレイアウトを適材適所で使えるようになりたい。

おわりに

個人的メモなので整理されてなく、雑メモですみません。(メモにキャプチャはないので頑張って貼りました)
まだ初歩的なところですが今後もSwiftUIと触れ合う時間を作って仲良くなっていこうと思います。

43
18
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?