カテゴリー: iOS

[iOS]意外と簡単ウィジェット機能を実装してみる

はじめに

こんにちは、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ライブラリを利用して実装を行います。

//通信処理
    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)
        
    }

レスポンス用のクラス。

//一月ごとのデータ取得
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 更新処理が失敗した時

    //ここで通信処理を記述システム側から更新処理を行うタイミングで呼ばれます。
    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で分岐を作るのが良さそうです。

    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)を設定します。

    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を呼び出すことでアプリの起動が可能です。

    @IBAction func didTapApp(_ sender: Any) {
        extensionContext?.open(URL(string: "WidgetTest://fromWidget/")!, completionHandler: nil)
    }

さいごに

アプリのウィジェットは今までユーザーが気づかないことがよくあり、あまり注目されていませんでした。iOS14でAndroidの様に使える様になったら、既存のアプリに組み込みたいという依頼も出てくるかもしれません。機能として便利だし可能性を感じるので、今後流行るといいなと思います。

おすすめ書籍

suzuki

シェア
執筆者:
suzuki
タグ: Swift

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

2週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

4週間 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前