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

この記事は [ZOZO Advent Calendar 2022 カレンダー](https://qiita.com/advent-calendar/2022/zozo)Vol.6の18日目の記事です。

## はじめに

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44142/d91371e8-e53f-b725-d237-47f87c642073.png" width=33%><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44142/89ada599-b166-171e-19a7-ccab0780b673.png" width=33%><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44142/b04b1096-fbaf-20b9-fa2c-f8cf565a09a7.png" width=33%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44142/6f89d454-1415-3057-7dfa-4c35db25c3eb.png" width=33%><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44142/06321b19-5ed7-b7b2-7288-a2a2d9ddbf00.png" width=33%><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44142/9ac14895-32c9-b88a-7c24-9bb278961ff1.png" width=33%>

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


## 環境
Xcode 14.1
iOS 16+

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44142/d6884d37-12f1-1497-9bf6-8c5fec4f650b.png" width=50%>


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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44142/ae074fdd-ceed-e169-6e3b-ed477cd5e612.png" width=50%>


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

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


### ScrollViewのcontentInsets指定
配下のViewにpaddingを指定する。
    
```swift
ScrollView(.horizontal, showsIndicators: false) {
    LazyHStack(spacing: 12) {
		    ...
		}
		.padding(.horizontal, 20)
}
```
    
### `@ViewBuilder` 属性を使ってViewを返す

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

```swift
@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が効いてしまうことがあった。

```swift
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を使う

```swift
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)
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44142/1a7be5f2-379a-015a-c669-0df4c44bd813.png" width=50%>

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

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

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

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

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

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

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


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

```swift
Text(state.caption)
    .lineLimit(nil)
```


### ライトグレーを使う

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


### ForEachでoffsetを使用する方法

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


### previewDevice

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44142/d36b34f2-f3a6-b9c1-bf4d-6459f2aa1a2a.png" width=50%>



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

```terminal
xcrun simctl list devicetypes
```

### LazyVStack内でListは使えない？

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

### ScrollViewでページングができない
UIScrollViewに存在した isPagingEnabled　と同等のものがない

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

![スクリーンショット 2022-12-16 18.06.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44142/219c9c65-68d3-af6f-3a50-5bd2acd2b4f3.png)


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

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