【watnowテックカレンダー2020/2/26】
今日はwatnowのryotaが担当します
よろしくお願いします
# はじめに
2020年9月にiOS14がリリースされ、その追加機能の一つとしてウィジェットという機能が利用可能になりました。
Apple公式のWidget Kitについて
Apple Introducing WidgetKit
ウィジェットはアプリの一部情報をiOSのホーム画面へ表示させられる機能です。この機能を用いればアプリを開かずにちょっとした情報を確認することができます。またウィジェットに任意の画面へのリンクを設定することもできユーザのアプリの利用率も向上させられます。
一方でWidgetKitを作成するにはswiftUIで実装する必要があり、なかなか導入しにくいと思う部分もあると思いますが、やってみると簡単に作成することができたので共有しようと思います。
Widget Kitを導入する
まずWidget Kitを導入するにあったってQ&Aです。(主に僕が思ってたことです)
-
Swiftでプロジェクトを作ってるけど、SwiftUIのWidget Kitってそもそも共存できるの?
-
SwiftUIはSwift言語で使用できるUI構築フレームワークということもあり問題なく共存できます。
ただしWidget KitはiOS14以降のみの対応のため、それより以前のバージョンの端末では使用できない点に注意してください。 -
Swiftしか触ったことないけどSwiftUIでコードを書くのはちょっと..
-
Swiftを書いたことのある方にとってSwiftUIはなんとなくであれば実装しやすい言語だと思います。swiftUIではソースコードと、iPhone画面を同時に表示するプレビュー機能(名称はCanvas)があるので簡単にViewを確認できます。Widget Kitを導入する場合でもそこまで複雑な処理やUIを実装しない限り、実装するコードは少なくてすみます。
-
Widgetにメモリ制限とかはあるの?
-
Widget Kitではホーム画面に常時設置されるという特性上、厳しいメモリ制限があるようです。トータルで最大30MB以上のデータはロードできないといった報告があるようです。似たような例としてApp extensionsを実装したこちらの記事によると、使用する機種によって使用できるメモリ量も増減するようです。いずれにせよ複雑なグラフの描画や、解像度の高いデータの取得などは行わない方が良さそうです。
-
メモリ制限とかについて
10 Tips on Developing iOS 14 Widgets
-
もしSwiftUIを初めて使うという方はまずは、公式のチュートリアルから始めるとわかりやすいです。
Appleが掲載している公式のSwiftUIチュートリアル
上記のSwiftUIチュートリアルを日本語で解説されているページもあります
導入
まずはWidgetKitのデータの取り扱い方についてはこちらの記事が非常にわかりやすくまとまっているので概要を掴むとわかりやすいです。
【iOS14】Widget(WidgetKit) まとめ
Widget Kitを導入していきます。
プロジェクトを作成したあとに、File=>New=>Target
真ん中らへんにWidget Extensionがあるのでクリックし、Next
Product Nameを適当に決めてFinish
するとProduct Name以下にこのようなファイルが出来上がっていると思います。
それぞれの役割として
- widgetDemo.swift
- =>データやViewとかの処理をまとめて行う。追加時には時刻を表示する場合のサンプルコードが記入されている。もし独自のウェイジェットを実装していくならViewとかでファイルを分けるとわかりやすい。
- widgetDemo.intendefinition
- => メインのアプリとデータを共有したりするときにここで設定する
- Assets.xcassets
- => 静的な画像データとかはここにおく
- info.plist
- => Widgetの設定プロパティ
以下のようにWidgetExtensionのViewが表示されています。
この状態で アプリのビルドを行い、ホーム画面を長押し=>widgetを追加する
これでホーム画面に現在の時間が表示されるウィジェットが追加されます。時計を表示させるだけなら実はこれだけで実装となります。
Simulator Screen Shot - iPod touch (7th generation) - 2021-02-26 at 11.59.45.png
APIからデータをとってきて表示させる
ここまでで、すでにホーム画面にWidgetを追加して、時間を表示させることができました。
ここをベースに開発していくことでアプリ側からデータを受け取って表示させたりサーバからデータを取得して表示させたりすることができます。
では今回はAPIからデータをとってきて表示させるWidgetを開発しようと思います。
完成はこんな感じです。今回はNewsAPIからJSONで技術系のニュースデータを取得してその1つめのデータをWidgetに追加するようにします。
まずはニュースを取得するためにNewsAPIを利用するのでそのAPIKeyを取得します。News系のAPIで探してみるとこれが多く使われてるみたいです。APIKeyを取得したら以下のようにGETするとレスポンスがかえってきます。
http://newsapi.org/v2/top-headlines?sources=techcrunch&apiKey=APIキー
WidgetKitがHTTP通信を行うことができるようにinfo.plistを編集してあげる必要があります。HTTPS通信が必須化しているためHTTP通信を行うには許可する必要があります。
今回編集するのはアプリのinfo.plistではなく、Widgetのフォルダに作成されたinfo.plistを編集する必要があることに注意してください。
HTTP通信の許可する方法についてはこちらが参考になります。
iOSアプリのhttp通信を許可する方法
まずはCodableなモデルを作成していきます。
JSONからモデルを作成する時は、Convert JSON into gorgeousが便利です。JSONを入力するだけでいい感じにモデルを作ってくれます。
import WidgetKit
import SwiftUI
import Intents
import Foundation
// MARK: - News
struct News: Codable {
let status: String?
let totalResults: Int?
let articles: [Article]?
}
// MARK: - Article
struct Article: Codable {
let source: Source?
let author, title, articleDescription: String?
let url: String?
let urlToImage: String?
let publishedAt: String?
let content: String?
enum CodingKeys: String, CodingKey {
case source, author, title
case articleDescription = "description"
case url, urlToImage, publishedAt, content
}
}
// MARK: - Source
struct Source: Codable {
let id: ID?
let name: Name?
}
enum ID: String, Codable {
case techcrunch = "techcrunch"
}
enum Name: String, Codable {
case techCrunch = "TechCrunch"
}
次はWidgetKitのメインとなる部分です。今回は最初からあるSimpleEntryにnewsDataの項目を追加しています。取得したデータからTimelineEntryを作成し、それをもとにTimelineを構成していくイメージです。
import WidgetKit
import SwiftUI
import Intents
struct Provider: IntentTimelineProvider {
@ObservedObject var NewsStore = SessionStore()
func placeholder(in context: Context) -> SimpleEntry {
#Widgetが読み込み中の時に呼ばれる
SimpleEntry(date: Date(), configuration: ConfigurationIntent(), newsData: [] as? News)
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
#Widgetを追加しようとするときに例として表示される
#ダミーのデータをいれるとよい
let entry = SimpleEntry(date: Date(), configuration: configuration, newsData: [] as? News)
completion(entry)
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
/// 1時間ごとに更新する
let refresh = Calendar.current.date(byAdding: .hour, value: 1, to: Date()) ?? Date()
NewsStore.fetch{ newData in
entries.append(SimpleEntry(date: Date(), configuration: ConfigurationIntent(), newsData: newData))
let timeline = Timeline(entries: entries, policy: .after(refresh))
completion(timeline)
}
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
let newsData:News?
}
struct newsWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack{
URLImageView(url: URL(string: entry.newsData?.articles?[0].urlToImage ?? "")!)
.aspectRatio(contentMode: .fill)
Text(entry.newsData?.articles?[0].title ?? "")
.foregroundColor(.black)
.font(.headline)
.padding(8)
}
}
}
@main
struct newsWidget: Widget {
let kind: String = "newsWidget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
newsWidgetEntryView(entry: entry)
}
.configurationDisplayName("News Widget")
.description("News Widgetの説明")
#大きなwidgetのみ対応する
.supportedFamilies([.systemLarge])
}
}
struct newsWidget_Previews: PreviewProvider {
static var previews: some View {
newsWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), newsData: [] as? News))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
データを取得するクラスはこんな感じで実装しています。
import Foundation
import Combine
final class SessionStore: ObservableObject {
@Published var current : News?
init(){
self.fetch{ val in
print(val)
}
}
}
extension SessionStore{
func fetch(completion : @escaping(News)->()){
NewsClient.fetchSummary {
self.current = $0
completion($0)
}
}
}
最後にURLから画像を読み込む処理についてです。
Swiftで非同期で画像を取得するときはKing Fisherとかが便利なんですが、今回はSwiftUIで実装するということもあり処理を別途書く必要があります。
import SwiftUI
struct URLImageView: View {
let url: URL
@ViewBuilder
var body: some View {
if let data = try? Data(contentsOf: url), let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
} else {
Image(systemName: "photo")
.resizable()
}
}
}
以上で、Widgetの実装は終わりになります。
これでWidgetを追加すると最新のニュースが表示されます。
今回使用したサンプルプロジェクトは以下になります。
https://github.com/ryota2425/widgetExample
最後に
WidgetKitはSwiftUIでの実装で難しそうに見えますが、ぜひチャレンジしてみてください。
今回はAPIの情報を表示させるだけでしたが、これを発展させてクリックした場にアプリで記事を表示させるとより実用的になっていくかと思います!