個人開発アプリにウィジェットを追加した話

9 min read読了の目安(約8300字

はじめに

こんにちは。先日個人開発したアプリにウィジェットを追加したので、その過程について書こうと思います。ウィジェットを追加したい方の参考になれば幸いです。また、SwiftUIの基本的なことについては、自分の過去の記事でコピペしてそのまま動くものを多く残しているのでそちらを参考にしていただければと思います。なお、書いている人は仕事としてアプリ開発を行ったことがありません。稚拙なコードや理解の浅い部分も散見されるかと思いますが、その際はコメントなどで指摘していただきたいです。

アプリの紹介



マンセルカラーから色を選択し、RGB値とカラーコード を取得するとても簡単なアプリです。全てSwiftUIで作っています。ソースコードや構成についてはこちらで書いています。

手順1 Widget Extensionを追加する

File -> New -> Targetから,Widget Extensionを選択し、適当(辞書通りの意味)な名前をつけます。すると、デフォルトで時刻を表示するウィジェットが追加されます。


時刻を表示するはたらきをしているのは以下の部分です。

struct TestWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)//ここ
    }
}

例えばこのコードを以下のようにすると

struct TestWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text("あ")
            Circle()
        }
    }
}


このようになります。

手順2 ウィジェットとしてはたらくモジュールのファイルを作る

ウィジェットを先ほどのようにEntryViewの中に直接書くことで見た目を変えることはできますが、多くの場合別のファイルでモジュールを作成し、それをEntryViewの中に入れます。(一つのファイルで全て行おうとするとエラーになるので。)

Widget Extensionのグループではなく、アプリ本体の方にファイルを作成し、Target MembershipにExtenision Widgetを追加します。

手順3 ウィジェットとしてはたらくモジュールを作る

ここまで行うと以下のようになっていると思います。

ここで、プレビューの画面をウィジェットの大きさに変更し、デザインしやすくします。

import SwiftUI

struct TestWidget: View {
    var body: some View {
        Text("Hello, World!")
    }
}

struct TestWidget_Previews: PreviewProvider {
    static var previews: some View {
        Group {
        TestWidget()
            .previewLayout(.fixed(width: 160, height: 160.0))//変更した
        }
    }
}

ウィジェットの大きさは端末の大きさによって微妙に異なりますが、SEの大きさの160にしました。

ここで作ったTestWidgetをEntryViewの中身に入れます。

struct WidgetofColorDesignAppEntryView : View {
    var entry: Provider.Entry
    
    var body: some View {
        TestWidget()//変えた
    }
}

手順4 モジュールの引数を設定する

※タイトルで引数と表現したのは、構造体のもつストアドプロパティのことを指しています。ただ、そのストアドプロパティの値に応じて画面が変化するので、引数と表現しています。

ウィジェットの理念として、「パーソナライズ化されている」、「動的である」というものがあります。表示するウィジェットがそれらの条件を満たすためには、引数を設定することがが一つの鍵になります。

僕の場合は、R,G,Bの値が格納された[Int]型の変数を引数にしており、それをもとに色や数字、カラーコードを表示させています。

import SwiftUI

struct WidgetColorView: View {
    var RGBArray: [Int]
    
    func radixJudge (_ n:Int) -> String {
        if n <= 15 {
            return "0"+String(n,radix: 16)
        } else {
            return String(n,radix: 16)
        }
    }
    
    
    var body: some View {
        VStack {
            Spacer()
                HStack {
                    WidgetView(rValue: RGBArray[0], gValue: RGBArray[1], bValue: RGBArray[2])
                    VStack(alignment: .leading, spacing: 8) {
                        HStack {
                            Circle()
                                .foregroundColor(Color.init(UIColor(red: 1, green: 0, blue: 0, alpha: 1)))
                                .frame(width: 18, height: 18)
                                
                            Text("\(RGBArray[0])")
                        }
                        
                        HStack {
                            Circle()
                                .foregroundColor(Color.init(UIColor(red: 0, green: 1, blue: 0, alpha: 1)))
                                .frame(width: 18, height: 18)
                            Text("\(RGBArray[1])")
                        }
                        HStack {
                            Circle()
                                .foregroundColor(Color.init(UIColor(red: 0, green: 0, blue: 1, alpha: 1)))
                                .frame(width: 18, height: 18)
                            Text("\(RGBArray[2])")
                        }
                    }
                }
            Spacer()
                Text(self.radixJudge(self.RGBArray[0])+(self.radixJudge( self.RGBArray[1]))+(self.radixJudge(self.RGBArray[2])))
                    .fontWeight(.bold)
                    .font(.largeTitle)
            Spacer()
        }
    }
}

struct WidgetColorView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            WidgetColorView(RGBArray: [0,0,0])
                .previewLayout(.fixed(width: 160, height: 160.0))
        }
    }
}

また、このモジュールに用いられている正方形は別のファイルで作成しています。

このモジュールではR,G,Bの3つの整数を引数(ストアドプロパティ)に設定しています。

画像

import SwiftUI
import Foundation

struct WidgetView: View {
    var rValue: Int
    var gValue: Int
    var bValue: Int
    
    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 8)
                .fill(Color.init(UIColor(red: CGFloat(rValue)/CGFloat(255), green: CGFloat(gValue)/CGFloat(255), blue: CGFloat(bValue)/CGFloat(255), alpha: 1)))
                .frame(width: 160/2, height: 160/2, alignment: .center)
        }
    }
}

struct WidgetView_Previews: PreviewProvider {
    static var previews: some View {
        WidgetView(rValue: 20, gValue: 84, bValue: 128)
    }
}

手順5 App Groupsを設定する

このアプリのウィジェットの機能はお気に入りに最後に保存した色を表示させるというものです。保存の方法にUserDefaultsを使っているので、アプリとウィジェットに共通のApp Groupsを設定します。
TARGETS -> Signing&Capabilities-> +CapabilityからApp Groupsを追加します。


そして、開発者の名前やアドレスから定まるものをグループ名に追加します。

手順6 タイムラインにエントリーする値を設定する

いよいよ本格的にウィジェットの設定に移ります。getTimelineの中に、モジュールの引数に使うための変数を定義し、UserDefaultsから値を持ってきた値を変数に代入します。

let userDefaults = UserDefaults(suiteName: "*****")
let lastSoterdArray = userDefaults?.array(forKey: "storedArray") as? [[Int]]

手順7 TimelineEntryに引数を設定する

SimpleEntryはデフォルトで以下のように設定されていると思います。

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
}

ここに、先ほどのモジュールの引数の型を指定します。

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
    let lastStoredArray: [[Int]]//追加した
}

すると、placeholdergetSnapshotに加えた値の初期値を設定するようエラーが出るので、適切な値を設定します。ほとんど用いられない値と思ったので、僕は[]にしました。

重要なのはgetTimelineです。ここでもentryの値を設定するよう求められるので、適切な値を設定します。この値はウィジェットを初めて追加した時に使われます。

let entry = SimpleEntry(
                date: entryDate,
                configuration: ConfigurationIntent(),
                lastStoredArray: lastStoredArray ?? [[0,0,0]]
            )

僕は上のようにしました。

手順8 対応するウィジェットの大きさを設定する

僕の場合は最も小さい大きさのウィジェットしか使わなかったので、ユーザがそれしか選択できないよう設定します。

supportedFamiliesに、対応させたい大きさを格納します。

struct WidgetofColorDesignApp: Widget {
    let kind: String = "WidgetofColorDesignApp"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind,
                            intent: ConfigurationIntent.self,
                            provider: Provider()) { entry in
            WidgetofColorDesignAppEntryView(entry: entry)
        }
        .supportedFamilies([.systemSmall])//追加した
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

完成!

画像などで説明せず審査に出したところ落ちてしまいましたが、その後画像や説明を加え再提出したところ通りました。

冒頭でも書きましたとおり、間違っているところなどがあればコメントしていただきたいです🙇‍♂️

参考資料

https://www.youtube.com/watch?v=wOrkcdeui4U&t=1326s
https://www.youtube.com/watch?v=151GCQlGsKg&list=LL&index=78
動画はやっぱりわかりやすいです。
https://medium.com/swlh/build-your-first-ios-widget-part-1-d2cecdd4020a
英語でしたが、こちらも丁寧にまとめられていました。