はじめに
こんにちは、suzukiです。本日はiOSのウィジェット機能について触れていこうと思います。
本機能はスクリーンショットのようにアプリを立ち上げなくても、アプリがユーザーに伝えたい情報の表示ができます。
本当かどうかわかりませんが、iOS14でAndroidの様にホームに表示することも可能になると紹介しているサイトもあり、今後が楽しみな機能です。
ガイドラインガイドラインに従う必要があるので一読した方がいいです。
OSごとのできること
度々のアップデートでできることが増えていった経緯もありOSと機種によって変わります。
iOS8以降
検索画面によるウィジェットの表示
iOS10以降
・検索画面でウィジェットの拡張表示
・3DTouchによるホームのウィジェット表示 → iPhone6s、6s Plus以降の3DTouchが必要
ウィジェットの実装
それでは実際にウィジェットを使ってみましょう。
最終的な目標として株価の様にチャートを表示し、ボタンをタップした際にはアプリの起動ができる様にします。
Today Extensionの追加
AppleWatchと同様にAddTargetから追加します。
ツールバーのEditorからAddTargetを選択
Today Extensionを選択
今回はTodayWidgetという名前で追加しました。
無事追加できると下記のようにファイルが追加されます。
Today ExtensionはViewControllerとStoryBoardで構成されているのでわかりやすいです。
Chartを表示
デフォルトで追加された内容でビルドを行うと下記の様に表示がされます。
今回は株価アプリの様にウィジェットの中にグラフを表示します。
以前の記事をもとにChartライブラリを利用して実装を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | //通信処理 func fetchOHLCData(completionHandler: @escaping (OHLCList) -> Void ){ guard let url = URL(string: "https://api.cryptowat.ch/markets/coinbase-pro/btcusd/ohlc?periods=86400") else { return } URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in guard let data = data else{ return } //JSONDecoder let decoder = JSONDecoder() //JSONDecoderでレスポンス→MonthOHLCListResultクラスとして変換 if let response = try? decoder.decode(MonthOHLCListResult.self, from: data){ print(response.result) DispatchQueue.main.async{ completionHandler(response.result) } } }.resume() } //終値のデータのみを取得し線グラフ用のデータを作成 func generateCloseData(_ ohlcList: OHLCList) -> LineChartData{ var closeLineChartData:[ChartDataEntry] = [] ohlcList.list.enumerated().forEach { (i,data) in closeLineChartData.append(ChartDataEntry(x: Double(i), y: data[4])) } let set1 = LineChartDataSet(entries: closeLineChartData, label: "Close") set1.lineWidth = 1.75 set1.setColor(.black) set1.highlightColor = .yellow set1.drawValuesEnabled = false set1.drawCirclesEnabled = false return LineChartData(dataSet: set1) } func setupLineChart(_ chart: LineChartView, data: LineChartData){ chart.dragEnabled = true chart.setScaleEnabled(true) chart.pinchZoomEnabled = true chart.setVisibleYRange(minYRange: data.yMax , maxYRange: data.yMax, axis: .left) // chart.setVisibleXRange(minXRange: 20, maxXRange: 50) // legendの設定 chart.legend.horizontalAlignment = .left chart.legend.verticalAlignment = .bottom chart.legend.orientation = .horizontal chart.legend.drawInside = false chart.legend.font = UIFont(name: "HelveticaNeue-Light", size: 10)! // 左の目盛りの設定 chart.leftAxis.labelFont = UIFont(name: "HelveticaNeue-Light", size: 10)! chart.leftAxis.spaceTop = 0.3 chart.leftAxis.spaceBottom = 0.3 chart.leftAxis.axisMinimum = 90 // chart.rightAxis.enabled = false chart.xAxis.enabled = false chart.data = data chart.setVisibleYRange(minYRange: data.yMax , maxYRange: data.yMax, axis: .left) } |
レスポンス用のクラス。
1 2 3 4 5 6 7 8 9 10 11 | //一月ごとのデータ取得 struct MonthOHLCListResult: Codable{ let result: OHLCList } struct OHLCList: Codable{ let list: [[Double]] private enum CodingKeys: String, CodingKey { case list = "86400" } } |
通信はwidgetPerformUpdateという元から設定されているデリゲートで行います。
今回は成功だけしか描画していないですがcompletionHandler()の引数には下記があります。
・newData更新を行いたい時
・noData 更新の必要がない時
・failed 更新処理が失敗した時
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | //ここで通信処理を記述システム側から更新処理を行うタイミングで呼ばれます。 func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) { // Perform any setup necessary in order to update the view. // If an error is encountered, use NCUpdateResult.Failed // If there's no update required, use NCUpdateResult.NoData // If there's an update, use NCUpdateResult.NewData fetchOHLCData { ohlcList in let closeLine = self.generateCloseData(ohlcList) //Chartにデータを渡す。 self.setupLineChart(self.lineChartView, data: closeLine) completionHandler(NCUpdateResult.newData) } } |
非表示になったタイミングで破棄されているので、ViewDidLoadなどのライフサイクルの呼ばれるタイミングは想像より多かったです。
もし通信でログイン処理などを行う場合はUserDefaultを利用して、不要な認証処理などを行わない様に設計した方が良さそうです。
あとはStoryboardで設定すると下記の様にチャートやラベルが表示できます。
表示を増やす
表示を増やすをタップした際に拡大するには下記の様にwidgetLargestAvailableDisplayModeに.expandedを設定します。
今回のコードでは表示の拡大だけ行いましたが表示する情報そのものを変更したい場合はextensionContext?.widgetActiveDisplayModeで分岐を作るのが良さそうです。
1 2 3 4 5 6 7 8 9 10 11 12 13 | override func viewDidLoad() { super.viewDidLoad() //追加 extensionContext?.widgetLargestAvailableDisplayMode = .expanded //extensionContext?.widgetActiveDisplayModeで現在がどちらに設定されているか取得できる } //表示を増やすをタップした際に呼ばれるデリゲート変更したいサイズに変更 func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) { switch activeDisplayMode { case .compact: preferredContentSize = maxSize case .expanded: preferredContentSize = CGSize(width: preferredContentSize.width, height: 400) } } |
サイズ自分で設定できます。今回は広げただけですので、この様な表示になりました。
ウィジェットをタップした際にアプリを起動する
通常のプロジェクト同様にボタンにアクションが設定できます。
アクションないでURLSchemeを呼び出すことでアプリの起動が可能です。
URLSchemeの設定
アプリの呼び出した際に情報を渡したい場合は
SceneDelegateに
func scene(_ scene: UIScene, openURLContexts URLContexts: Set
を設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) { guard let url = URLContexts.first?.url else { return } // ホスト名 guard let components = URLComponents(string: url.absoluteString), let host = components.host else { return } if host == "fromWidget" { print(components) } } |
ウィジェットで下記の様にURLSchemeを呼び出すことでアプリの起動が可能です。
1 2 3 | @IBAction func didTapApp(_ sender: Any) { extensionContext?.open(URL(string: "WidgetTest://fromWidget/")!, completionHandler: nil) } |
さいごに
アプリのウィジェットは今までユーザーが気づかないことがよくあり、あまり注目されていませんでした。iOS14でAndroidの様に使える様になったら、既存のアプリに組み込みたいという依頼も出てくるかもしれません。機能として便利だし可能性を感じるので、今後流行るといいなと思います。