WidgetのオススメPreview一覧(iOS)

## はじめに

本記事は [iOS Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ios) の10日目の記事です。
昨日は @417_72ki さんで [SwiftのKeyPathと戯れる](https://qiita.com/417_72ki/items/7f3a6f651e48c9c1b209) でした。

iOS 14で追加されたWidgetのオススメPreviewを紹介します。

## 環境

- OS：macOS Big Sur 11.0.1
- Xcode：12.2 (12B45b)
- Swift：5.3.1

## 対象コード

対象WidgetのEntryViewとEntryのコードを載せます。
長いので折りたたみにします。
本筋ではないので読み飛ばしても構いません。

<details><summary>EntryViewのコード</summary><div>

```swift:MonsterEntryView.swift
import WidgetKit
import SwiftUI

struct MonsterEntryView: View {
    var entry: MonsterWidget.Provider.Entry
    @Environment(\.widgetFamily) private var family

    var body: some View {
        switch family {
        case .systemSmall:
            ZStack {
                Color(.systemBackground)
                VStack {
                    icon
                    Spacer(minLength: 8.0)
                    name
                }
                .padding()
            }
        case .systemMedium:
            ZStack {
                Color(.systemBackground)
                HStack {
                    VStack {
                        icon
                        Spacer(minLength: 8.0)
                        name
                    }
                    Spacer(minLength: 16.0)
                    description
                }
                .padding()
            }
        case .systemLarge:
            EmptyView()
        @unknown default:
            EmptyView()
        }
    }

    private var icon: some View {
        Image(uiImage: entry.icon)
            .resizable()
            .aspectRatio(contentMode: .fit)
    }

    private var name: some View {
        Text(entry.name)
            .font(.headline)
    }

    private var description: some View {
        Text(entry.description)
            .font(.body)
    }
}
```

</div></details>

<details><summary>Entryのコード</summary><div>

```swift:MonsterWidget.swift
struct MonsterWidget: Widget { ... }

extension MonsterWidget {
    struct Entry: TimelineEntry {
        let date: Date
        let name: String
        let description: String
        let icon: UIImage

        static func createDefault() -> Entry {
            .init(
                date: Date(),
                name: "uhooi",
                description: "ゆかいな　みどりの　せいぶつ。\nわるそうに　みえるが　むがい。",
                icon: UIImage(named: "Uhooi")!
            )
        }
    }
}
```

</div></details>

Previewに不要なWidgetとProviderのコードは省略します。
参考リンクにあるウホーイ図鑑のコードをご参照ください。

## WidgetのオススメPreview一覧

WidgetのオススメPreview一覧を紹介します。

### プレースホルダー

プレースホルダーの表示をプレビューします。
EntryViewに `.redacted(reason: .placeholder)` を付けると、プレースホルダーで表示します。

__コード__

```swift:MonsterEntryView.swift
struct MonsterEntryView_Previews: PreviewProvider {
    static var previews: some View {
        // プレースホルダー
        MonsterEntryView(entry: .createDefault())
            .redacted(reason: .placeholder) // !!!
            .previewContext(WidgetPreviewContext(family: .systemSmall)) // Mediumの場合は `.systemMedium` を指定する
        }
    }
}
```

__プレビュー__

|Small|Medium|
|:--|:--|
|![スクリーンショット 2020-12-10 22.05.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/de5371a7-4e72-965d-7398-e57514b50d05.png)|![スクリーンショット 2020-12-10 22.06.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/a3c27be9-ee75-1b44-d75c-2c7cca375577.png)|

### 通常

通常の表示をプレビューします。
プレースホルダーのプレビューから `.redacted(reason: .placeholder)` を取り除くのみです。

__コード__

```swift:MonsterEntryView.swift
struct MonsterEntryView_Previews: PreviewProvider {
    static var previews: some View {
        // 通常
        MonsterEntryView(entry: .createDefault())
            .previewContext(WidgetPreviewContext(family: .systemSmall))
        }
    }
}
```

__プレビュー__

|Small|Medium|
|:--|:--|
|![スクリーンショット 2020-12-10 22.05.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/120a3675-d636-0bfb-8e99-a8540b88da4d.png)|![スクリーンショット 2020-12-10 22.06.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/bc9aa5d2-9b80-8c05-a8fb-367dbd07c1a5.png)|

### ダークモード

ダークモードの表示をプレビューします。
通常のプレビューに `.environment(\.colorScheme, .dark)` を付けるのみです。

__コード__

```swift:MonsterEntryView.swift
struct MonsterEntryView_Previews: PreviewProvider {
    static var previews: some View {
        // ダークモード
        MonsterEntryView(entry: .createDefault())
            .environment(\.colorScheme, .dark) // !!!
            .previewContext(WidgetPreviewContext(family: .systemSmall))
        }
    }
}
```

__プレビュー__

|Small|Medium|
|:--|:--|
|![スクリーンショット 2020-12-10 22.05.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/f1beec50-4cf9-1643-88b5-0fb6571d75eb.png)|![スクリーンショット 2020-12-10 22.06.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/41647a04-db41-ccb8-c479-a43ce06ecad8.png)|

### 短い文字列

短い文字列の表示をプレビューします。
Previewを使うと境界値の表示もかんたんに確認できます。

__コード__

```swift:MonsterEntryView.swift
struct MonsterEntryView_Previews: PreviewProvider {
    typealias Entry = MonsterWidget.Entry

    static var previews: some View {
        // 短い文字列
        MonsterEntryView(entry: createShortEntry())
            .previewContext(WidgetPreviewContext(family: .systemSmall))
        }
    }

    private static func createShortEntry() -> Entry {
        .init(
            date: Date(),
            name: "1",
            description: "1",
            icon: UIImage(named: "Uhooi")!
        )
    }
}
```

__プレビュー__

|Small|Medium|
|:--|:--|
|![スクリーンショット 2020-12-10 22.06.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/812f9ddd-6c21-7239-6707-28f0da639571.png)|![スクリーンショット 2020-12-10 22.06.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/1e6bac51-0e9a-a2ef-a0f7-430d966d75f2.png)|

Mediumで `1` が右寄せなのが気になります。
おそらく `HStack` と `Spacer()` を使えば左寄せにできますが、短い文字列が入ることを想定していないので、このままとしています。

### 長い文字列

長い文字列の表示をプレビューします。
短い文字列と同様の方法です。

__コード__

```swift:MonsterEntryView.swift
struct MonsterEntryView_Previews: PreviewProvider {
    typealias Entry = MonsterWidget.Entry

    static var previews: some View {
        // 長い文字列
        MonsterEntryView(entry: createLongEntry())
            .previewContext(WidgetPreviewContext(family: .systemSmall))
        }
    }

    private static func createLongEntry() -> Entry {
        .init(
            date: Date(),
            name: "123456789012345678901234567890",
            description: "12345678901234567890\n12345678901234567890\n12345678901234567890",
            icon: UIImage(named: "Uhooi")!
        )
    }
}
```

__プレビュー__

|Small|Medium|
|:--|:--|
|![スクリーンショット 2020-12-10 22.06.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/70753a85-a822-5c29-9a08-2806b6bc43e1.png)|![スクリーンショット 2020-12-10 22.07.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/23706b4c-14c6-44b1-b70b-115771ac404d.png)|

Small・Mediumの両方で画像が小さくなるのが気になりますが、モンスター名が長くなることを想定していないので、このままとしています。
「…」になるのも気になりますが、同様の理由でこのままとしています。

### 全体図

上記のコードをいい感じにまとめました。

```swift:MonsterEntryView.swift
struct MonsterEntryView_Previews: PreviewProvider {
    typealias Entry = MonsterWidget.Entry

    static var previews: some View {
        ForEach(families.indices) { index in
            previewEntryViewGroup
                .previewContext(WidgetPreviewContext(family: families[index]))
        }
    }

    private static let families: [WidgetFamily] = [.systemSmall, .systemMedium]

    private static var previewEntryViewGroup: some View {
        Group {
            MonsterEntryView(entry: .createDefault())
                .redacted(reason: .placeholder)
            MonsterEntryView(entry: .createDefault())
            MonsterEntryView(entry: .createDefault())
                .environment(\.colorScheme, .dark)
            MonsterEntryView(entry: createShortEntry())
            MonsterEntryView(entry: createLongEntry())
        }
    }

    private static func createShortEntry() -> Entry {
        .init(
            date: Date(),
            name: "1",
            description: "1",
            icon: UIImage(named: "Uhooi")!
        )
    }

    private static func createLongEntry() -> Entry {
        .init(
            date: Date(),
            name: "123456789012345678901234567890",
            description: "12345678901234567890\n12345678901234567890\n12345678901234567890",
            icon: UIImage(named: "Uhooi")!
        )
    }
}
```

## おわりに

これでWidgetのさまざまなパターンのPreviewをまとめて確認できます！
もっといい方法や、他にも確認すべきパターンがあれば、コメントなどで教えていただけると嬉しいです :relaxed: 

以上、 [iOS Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ios) の10日目の記事でした。
明日は @MilanistaDev さんの記事です。

## 参考リンク

- [UhooiPicBook/UhooiPicBookWidgets at v1.3.0 · uhooi/UhooiPicBook](https://github.com/uhooi/UhooiPicBook/tree/v1.3.0/UhooiPicBookWidgets)
- [Refactor widget preview by uhooi · Pull Request #147 · uhooi/UhooiPicBook](https://github.com/uhooi/UhooiPicBook/pull/147)