Tech Blog

グローバルな家族アプリFammを運営するTimers inc (タイマーズ) の公式Tech Blogです。弊社のエンジニアリングを支える記事を随時公開。エンジニア絶賛採用中!→ https://timers-inc.com/engineering

iOS 14 Widgetをリリースした話 #iOS14

2回目のTechBlog投稿になります。桐山です。 外は寒くなってきたのに、部屋の日当たりが良すぎて部屋が暑いのが最近の悩みです。

iOS 14の目玉機能と言っても過言ではないWidget機能をFammでもリリースしたので、リリースまでの話とそれからの話をしてみようと思います。

リリースしたもの

思い出クイズ

Small Medium Large
f:id:timers-tech:20201119102821p:plain:w200 f:id:timers-tech:20201119102844p:plain:w200 f:id:timers-tech:20201119102905p:plain:w200

いつの写真?

  • アップロードされている写真の中からランダムに1枚を表示
  • タップすると写真をフルスクリーンで表示
  • 写真の切り替えはiOSのタイミングに依存

実際どれくらい使われているの?

Widgetをホーム画面に設置したユーザーの割合は、WidgetをリリースしてからのアクティブUUで計算すると約4.5%となっています。 また、圧倒的にSmallサイズの設置が多いです。

なぜ「いつの写真?」なのか

Widget機能に興味があるメンバーで、

  • 無料ユーザーでも楽しめるもの
  • タップしてアプリに来てもらえそうなもの

縛りのブレストを行った結果、十数種類のアイデアが出ました。

「いつの写真?」以外に出たアイデアを少しだけ低解像度版で紹介します。

メッセージ カレンダーPR アップロード訴求
f:id:timers-tech:20201119193904p:plain:w100 f:id:timers-tech:20201119193907p:plain:w100 f:id:timers-tech:20201119193910p:plain:w100

社内ヒアリングで「いつの写真」クイズが好評であったこと、リリースまでの時期が短かったため、比較的シンプルに作れる内容であることを加味し、Widgetの1stリリースは「思い出クイズ」になりました。
クイズ形式にすることで、Widgetをタップしてもらいやすくなり、アプリを開いてくれることの期待もしています。

Human Interface Guidelinesと比較してできていないこと

AppleのHIGには以下のように記載がありますが、現在の1種類のWidgetだけで全てを満たすことはもちろんできていません。

  • 付加価値が得られる場合は、ウィジェットを複数のサイズで提供してください
  • 1日を通して変化する動的な情報を優先します
  • 誕生日や休日などの意味のある機会に表示するような、驚きと喜びの機会を探してください

前述のブレストで出たアイデアの中には、上記のポイントを押さえた内容のものもあります。
現時点では、Widgetの存在が直接的にアクティブユーザーを増やす証明もできないですし、ROI(費用対効果)についても不透明な部分があります。
そのため、現時点では他の種類のWidgetを追加するかは未確定の状態となっています。(エンジニアとしては是非やりたい💪)

サンプルコード

f:id:timers-tech:20201119191353p:plain:w150

  • SmallサイズのWidgetをプレビューできるサンプルです
  • コピペそのままでも動作するはず
  • UIImageは適宜任意の画像に差し替えてください
import SwiftUI
import WidgetKit

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> HogeEntry {
        HogeEntry(
            title: "",
            color: .orange,
            image: UIImage(),
            date: Date()
        )
    }

    func getSnapshot(in context: Context, completion: @escaping (HogeEntry) -> ()) {
        let entry = HogeEntry(
            title: "",
            color: .orange,
            image: UIImage(),
            date: Date()
        )
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let timeline = Timeline(
            entries: [
                HogeEntry(
                    title: "",
                    color: .orange,
                    image: UIImage(),
                    date: Date()
                )
            ],
            policy: .atEnd
        )
        completion(timeline)
    }
}

struct HogeEntry: TimelineEntry {
    let title: String
    let color: Color
    let image: UIImage
    let date: Date
}

struct WidgetEntryView: View {
    var entry: Provider.Entry

    @Environment(\.widgetFamily) private var widgetFamily

    var body: some View {
        switch widgetFamily {
        case .systemSmall:
            GeometryReader { geometry in
                VStack(spacing: 0) {
                    Text(entry.title)
                        .font(.title)
                        .frame(width: geometry.size.width, height: 36)
                        .foregroundColor(.white)
                        .background(entry.color)

                    Image(uiImage: entry.image)
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(
                            width: geometry.size.width,
                            height: geometry.size.height - 36
                        )
                        .clipped()
                }
            }
        @unknown default:
            VStack {}
        }
    }
}

@main
struct FooWidget: Widget {
    let kind: String = "hoge"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            WidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Widgetを追加する画面のタイトル")
        .description("Widgetを追加する画面の説明文")
    }
}

struct FooWidget_Previews: PreviewProvider {
    static var previews: some View {
        WidgetEntryView(
            entry: Provider.Entry(
                title: "タイトル",
                color: .orange,
                image: UIImage(),
                date: Date()
            )
        )
        .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

Xcode 12 Beta版での不具合

Widgetのプレビューが表示されない

Xcode 12 Beta 4から、Widgetのプレビューが表示されない事象を確認しました。
Xcode 12.0.1にアップデートした後は問題なくプレビューできています。

感想

個人的に普段写真を撮ってもほとんど見返すことがないのですが、「ホーム画面を開く」という毎日何度もする操作の中で過去の写真を振り返ることができる体験はすごく良いなと感じました👏

公開から6週間で、iOS 14がiOS 13の普及率を上回ったようです。 iphone-mania.jp
まだまだ生まれたばかりのWidget機能。今後の発展がとても楽しみです🐰

おまけ:個人アプリでのWidget対応小ばなし

個人アプリでMOGIRIというアプリをリリースしており、こちらもWidget対応を行いました。

f:id:timers-tech:20201119195829p:plain:w500

アプリ本体では地図を表示する方法として、WKWebViewにHTMLのbodyにSVG形式の地図データを読み込ませ、JavaScript実行で都道府県や国に色を塗っているのですが、WidgetではWebViewが使用できません。
そのため、PocketSVGというライブラリでSVGからCAShapeLayerに変換し、UIImageで表示するという荒技を使っています。

PR

子育て家族アプリFammを運営するTimers inc.では、現在エンジニアを積極採用中!
急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください!
採用HP: http://timers-inc.com/engineerings

Timersでは各職種を積極採用中!

急成長スタートアップで、最高のものづくりをしよう。

募集の詳細をみる