この記事では、SwiftUIでのWidget_iOS14.0のConfigtation Intentを利用した実装方法、そしてより実践的な利用方法としてCoreDataとの連携方法を紹介しています。
なおConfigtation Intentを利用しない実装方法は、こちらの記事で解説しています。
Configtation Intentを利用する実装方法でも必要となる、AppGroupの作成方法などはこちらの解説を参考にしてください。
この記事では、SwiftUIでのWidget_iOS14.0の実装方法、そしてより実践的な利用方法としてCoreDataとの連携方法を紹介しています。 ◆動作検証環境・ローカル環境:mac Catalina[…]
◆動作検証環境
・XCode:12.1
・SwiftUI:2.0
・iOS:14.0
・Life Cycle:SwiftUI App
Widget_iOS14.0(Configtation Intent)の実装方法【Widget機能の追加】
Widget機能を利用するには、まずホストとなるApp(Containing Appと呼んだりしますが、この記事ではホストAppとして説明しています)が必要となります。
通常の方法でプロジェクトを作成するか、Widget機能を利用するプロジェクトを開きます。
今回は以前の記事で紹介した、CoreDataを利用するプロジェクトの中で、Widgetを追加します。
対象のプロジェクトはこちらの記事を参考にしてください。
この記事では、SwiftUIでのCoreData の基本的な実装方法【CRUD(作成、更新、削除)】を紹介しています。簡単なタスク管理App(タスク内容とスケジュールの登録)を作成しながら解説します。 ◆動[…]
Configtation Intentを利用する場合、Targetの新規作成の際に、Include Configtation Intentにチェックを入れます。
Widget_iOS14.0(Configtation Intent)の実装方法【Widgetの表示確認】
Widgetの追加を行ったら、ホーム画面でWidgetの表示確認を行います。
ビルドするターゲットが追加したWidgetになっている事を確認し、ビルドします。
デフォルトのコードでは、Configtation Intentを利用しない場合と同様に時刻が表示されますが、Widgetを長押しすると編集用のボタンが表示されます。
こちらをタップすると、データをより詳細に表示するなどの機能を実装できます(デフォルトの状態では編集画面が表示されるだけです)。
デフォルトのWidgetのファイル(この記事のサンプルではTestIntentWidget.swift)のデフォルトのコードは以下のとおりです。
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 63 64 | import WidgetKit import SwiftUI import Intents struct Provider: IntentTimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), configuration: ConfigurationIntent()) } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), configuration: configuration) completion(entry) } func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate, configuration: configuration) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } struct SimpleEntry: TimelineEntry { let date: Date let configuration: ConfigurationIntent } struct TestIntentWidgetEntryView : View { var entry: Provider.Entry var body: some View { Text(entry.date, style: .time) } } @main struct TestIntentWidget: Widget { let kind: String = "TestIntentWidget" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in TestIntentWidgetEntryView(entry: entry) } .configurationDisplayName("My Widget") .description("This is an example widget.") } } struct TestIntentWidget_Previews: PreviewProvider { static var previews: some View { TestIntentWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent())) .previewContext(WidgetPreviewContext(family: .systemSmall)) } } |
Widget_iOS14.0(Configtation Intent)の実装方法【WidgetとCoreDataの連携】
Configtation Intentを利用するWidgetとCoreDataを連携させるには、AppGroupを利用します。
詳しくは、以前の記事を参考にしてください。
【SwiftUI】Widget_iOS14.0の実装方法【CoreDataとの連携】
今回は、CoreDataに登録されているTaskの各データをWidgetに表示し、それぞれのデータを選択するとホストAppの詳細画面に遷移する方法を解説します(通常はWidgetをタップするとホストAppのホームViewへ遷移する)。
ホストAppの編集
上記の動きに対応するために、ホストAppを以下のように編集します。
- CoreData内のTaskエンティティにユニークな情報(UUID)の追加
- 詳細画面表示の実装
CoreData内のTaskエンティティにユニークな情報(UUID)の追加
CoreDataTest.xcdatamodeld を開き、AttributesにUUID 型のid を追加します。
CoreDataのUUID型のidの場合、データが新規に追加されても、オートインクリメントでidが追加されません。
そのため、データ書き込み時にUUID型idを追加する処理を加えます。
データ書き込みのメソッドがあるViewModel.swift を編集します。
ViewModel.swift
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 | func writeData(context : NSManagedObjectContext ){ if updateItem != nil{ updateItem.date = date updateItem.content = content try! context.save() updateItem = nil isNewData.toggle() content = "" date = Date() return } let newTask = Task(context: context) newTask.date = date newTask.content = content newTask.id = UUID() //UUID型id追加 do{ try context.save() isNewData.toggle() content = "" date = Date() } catch{ print(error.localizedDescription) } } |
詳細画面表示の実装
まずは、詳細画面Viewの作成
DetailView.swift
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 | import SwiftUI struct DetailView: View { @ObservedObject var viewModel : ViewModel @State var content: String @State var date: Date @State var id:UUID var body: some View { VStack{ Text("Detail") .font(.title) .fontWeight(.bold) Divider() Text("id:\(id)") .padding() Text("content:\(content)") .font(.title) .padding() Text("date:\(date, style: .date)") .font(.title) .padding() Spacer() } } } |
次に、ContentView.swiftにHomeViewから詳細Viewへ遷移させるコードを追加します。
ContentView.swift
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | import SwiftUI import CoreData struct ContentView: View { @StateObject var viewModel = ViewModel() @FetchRequest(entity: Task.entity(), sortDescriptors: [NSSortDescriptor(key: "date", ascending: true)],animation: .spring()) var results : FetchedResults<Task> @Environment(\.managedObjectContext) var context var body: some View { NavigationView{ ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom), content: { VStack(spacing:0){ HStack{ Text("Tasks") .font(.largeTitle) .fontWeight(.heavy) .foregroundColor(.black) Spacer(minLength: 0) } .padding() .padding(.top,UIApplication.shared.windows.first?.safeAreaInsets.top) .background(Color.white) if results.isEmpty{ Spacer() Text("No Tasks") .font(.title) .foregroundColor(.primary) .fontWeight(.heavy) Spacer() }else{ ScrollView(.vertical,showsIndicators: false, content:{ LazyVStack(alignment: .leading, spacing: 20){ ForEach(results){task in NavigationLink(destination:DetailView(viewModel: viewModel, content: task.content ?? "", date: task.date ?? Date(), id: task.id ?? UUID())){ VStack(alignment: .leading, spacing: 5, content: { Text(task.content ?? "") .font(.title) .fontWeight(.bold) Text(task.date ?? Date(),style: .date) .fontWeight(.bold) Divider() }) } .foregroundColor(.primary) .contextMenu{ Button(action: { viewModel.EditItem(item: task) }, label: { Text("Edit") }) Button(action: { context.delete(task) try! context.save() }, label: { Text("Delete") }) } } } .padding() }) } } Button(action: {viewModel.isNewData.toggle()}, label: { Image(systemName: "plus") .font(.largeTitle) .foregroundColor(.white) .padding(20) .background(Color.green) .clipShape(Circle()) }) .padding() }) } .sheet(isPresented: $viewModel.isNewData, content: { NewDataSheet(viewModel: viewModel) }) } } |
WidgetExtensionの編集
CoreDataのデータをWidgetでも利用できるように、下記のように編集します。
CoreDataのタスクをWidgetに表示します。
TestIntentWidget.swift
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | import WidgetKit import SwiftUI import Intents import CoreData struct Provider: IntentTimelineProvider { var moc = PersistenceController.shared.managedObjectContext init(context : NSManagedObjectContext) { self.moc = context } func placeholder(in context: Context) -> SimpleEntry { var task:[Task]? let request = NSFetchRequest<Task>(entityName: "Task") request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)] do{ let result = try moc.fetch(request) task = result } catch let error as NSError{ print("Could not fetch.\(error.userInfo)") } return SimpleEntry(date: Date(), task: task!, configuration: ConfigurationIntent()) } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { var task:[Task]? let request = NSFetchRequest<Task>(entityName: "Task") request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)] do{ let result = try moc.fetch(request) task = result } catch let error as NSError{ print("Could not fetch.\(error.userInfo)") } let entry = SimpleEntry(date: Date(), task: task!, configuration: configuration) completion(entry) } func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] var task:[Task]? let request = NSFetchRequest<Task>(entityName: "Task") request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)] do{ let result = try moc.fetch(request) task = result } catch let error as NSError{ print("Could not fetch.\(error.userInfo)") } // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate, task: task!, configuration: configuration) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } struct SimpleEntry: TimelineEntry { let date: Date let task:[Task] let configuration: ConfigurationIntent } struct TestIntentWidgetEntryView : View { var entry: Provider.Entry var body: some View { VStack{ Text(entry.date, style: .time) Text(entry.task.first?.content ?? "") } } } @main struct TestIntentWidget: Widget { let kind: String = "TestIntentWidget" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider(context: PersistenceController.shared.managedObjectContext)) { entry in TestIntentWidgetEntryView(entry: entry) .environment(\.managedObjectContext, PersistenceController.shared.managedObjectContext) } .configurationDisplayName("My Widget") .description("This is an example widget.") } } |
これでWidgetが下記のように表示されるようになります。
Widget_iOS14.0(Configtation Intent)の実装方法【Widget編集画面の編集】
Widgetを長押しした際の編集画面で、登録しているタスクのデータ一覧を表示させ、それぞれをタップするとホストAppの詳細画面へ遷移するように編集します。
Widget機能を追加した際にフォルダ内に作成されている.intentdefinition ファイルを編集します。
デフォルトの状態は以下のようなコードになっています。
TestIntentWidget.intentdefinition
Widgetの編集画面を開いた際に、登録されているCoreDataのidの一覧が表示されるように、このファイルを編集します。
Options are provided dynamicallyにチェックを入れる事で、対象の値から選択できるようになります。
この状態でWidgetを長押しすると、編集内容が反映されています。
しかし、UUIDのボタンをタップしても下記のようにメッセージが表示され利用できません。
Intens機能の追加
新規Target追加から[Intents Extension]を選択します。
次に、Product Nameを記入し作成します。
次に作成したIntentのTargetページで編集を行います。
[General] – [Supported Intents]にConfigurationIntent のclass Nameを追加します。AuthenticationはNoneのままでOKです。
作成したclassを確認するには、作成したIntentフォルダ内のinfo.plistを確認します。
[NSExtension] – [NSExtensionAttributes] – [intentsSupported] – [Item 0]のValuで確認できはずです。
この作業を行わないと、
Unable To Install “CoreDataTest” のようなエラーメッセージが表示され、ビルドできません。
これでTestIntents フォルダが作成されます。
その中にあるIntentHandler.swift を以下のように編集します。
まずは、ホストAppのCoreDataに関係するファイルのTargetを確認します。
新たに作成された、TestIntents もTargetに追加します。
IntentHandler クラスを作成して下記のように編集します。
1 2 3 4 5 6 | import Intents class IntentHandler: INExtension, ConfigurationIntentHandling{ } |
このように、ConfigurationIntentHandling を継承させると、下記のように.intentdefinition の内容に応じXCodeから候補が出させるので、[Fix]します。
Type 'IntentHandler' does not conform to protocol 'ConfigurationIntentHandling' Do you want to add protocol stubs?
本記事の内容では、以下のようなコードとなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import Intents class IntentHandler: INExtension,ConfigurationIntentHandling { func provideUUIDOptionsCollection(for intent: ConfigurationIntent, with completion: @escaping (INObjectCollection<NSString>?, Error?) -> Void) { <#code#> } override func handler(for intent: INIntent) -> Any { // This is the default implementation. If you want different objects to handle different intents, // you can override this and return the handler you want for that particular intent. return self } } |
さらに作成されたメソッドの中に、CoreDataのデータを利用できるように編集を行います。
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 | import WidgetKit import SwiftUI import CoreData import Intents class IntentHandler: INExtension,ConfigurationIntentHandling { var moc = PersistenceController.shared.managedObjectContext func provideUUIDOptionsCollection(for intent: ConfigurationIntent, with completion: @escaping (INObjectCollection<NSString>?, Error?) -> Void) { let request = NSFetchRequest<Task>(entityName: "Task") var nameIdentifiers:[NSString] = [] do{ let results = try moc.fetch(request) for result in results{ nameIdentifiers.append(NSString(string: result.id?.uuidString ?? "")) } } catch let error as NSError{ print("Could not fetch.\(error.userInfo)") } let allNameIdentifiers = INObjectCollection(items: nameIdentifiers) completion(allNameIdentifiers,nil) } override func handler(for intent: INIntent) -> Any { return self } } |
これで、Widgetを確認すると以下のように、CoreDataに登録されているUUIDの一覧が確認できるようになります。
Widget_iOS14.0(Configtation Intent)の実装方法【WidgetからホストAppの詳細画面への遷移】
WidgetからホストAppの詳細画面に遷移させるには、DeepLink機能を利用します。
DeepLink機能のおおまかな流れは、
- Widgetの編集画面で選択したボタンでWidgetURLを選択する
- ホストAppでonOpenURLが呼ばれて対象のページへ遷移する
となります。
Widget側の編集
まず、TestIntentWidgetType.swift を作成し、TestIntentWidgetType プロトコルの中でURLを選択するためのメソッドを作ります。
TestIntentWidgetType.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import SwiftUI protocol TestIntentWidgetType { func makeURLScheme(id: UUID) -> URL? } extension TestIntentWidgetType where Self: View { func makeURLScheme(id: UUID) -> URL? { guard let url = URL(string: "CoreDataTest://detail") else { return nil } var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) urlComponents?.queryItems = [URLQueryItem(name: "id", value: id.uuidString)] return urlComponents?.url } } |
これで、CoreData内のそれぞれのデータに、CoreDataTest://detail?id=xxxxxxxxxxxxxx というようにURLを指定できます。
TargetMembershipでWidgetを追加もします。
続いて、TestIntentWidget.swift の編集です。
struct TestIntentWidgetEntryView の部分を以下のように編集します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct TestIntentWidgetEntryView : View,TestIntentWidgetType { //TestIntentWidgetType追加 var entry: Provider.Entry var body: some View { VStack{ Text(entry.date, style: .time) Text(entry.task.first?.content ?? "") } .widgetURL(makeURLScheme(id: (entry.task.first?.id)!)) //widgetURL追加 } } |
ホストApp側の編集
ViewModelの編集
以下のコードをViewModel に追加します。
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 | enum Inputs { case openFromWidget(url: URL) } @Published var activeId: UUID? func apply(inputs: Inputs) { switch inputs { case .openFromWidget(let url): if let selectedId = getWidgetItemID(from: url) { activeId = selectedId } } } func getWidgetItemID(from url: URL) -> UUID? { guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true), urlComponents.scheme == "CoreDataTest", urlComponents.host == "detail", urlComponents.queryItems?.first?.name == "id", let idValue = urlComponents.queryItems?.first?.value else { return nil } return UUID(uuidString: idValue) } |
次に、ホストAppでデータ一覧表示から詳細画面へ遷移する際のNavigationLink を編集します。
1 2 3 4 | NavigationLink(destination:DetailView(viewModel: viewModel, content: task.content ?? "", date: task.date ?? Date(), id: task.id ?? UUID()),tag:task.id ?? UUID(),selection: $viewModel.activeId){ } |
というように、tag 、selection を追加します。
そして、NavigationLink のあるViewにonOpenURL でViewModelで作ったapply メソッドを使ってモディファイアします。
1 2 3 4 5 | .onOpenURL{ url in viewModel.apply(inputs: .openFromWidget(url: url)) } |
これで、Widgetをタップすると、CoreDataの1番目のデータの詳細画面に遷移するようになります。
ここまでの編集を行い、ContentView.swiftの全コードは以下のようになっております。
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | import SwiftUI import CoreData struct ContentView: View { @StateObject var viewModel = ViewModel() @FetchRequest(entity: Task.entity(), sortDescriptors: [NSSortDescriptor(key: "date", ascending: true)],animation: .spring()) var results : FetchedResults<Task> @Environment(\.managedObjectContext) var context var body: some View { NavigationView{ ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom), content: { VStack(spacing:0){ HStack{ Text("Tasks") .font(.largeTitle) .fontWeight(.heavy) .foregroundColor(.black) Spacer(minLength: 0) } .padding() .padding(.top,UIApplication.shared.windows.first?.safeAreaInsets.top) .background(Color.white) if results.isEmpty{ Spacer() Text("No Tasks") .font(.title) .foregroundColor(.primary) .fontWeight(.heavy) Spacer() }else{ ScrollView(.vertical,showsIndicators: false, content:{ LazyVStack(alignment: .leading, spacing: 20){ ForEach(results){task in NavigationLink(destination:DetailView(viewModel: viewModel, content: task.content ?? "", date: task.date ?? Date(), id: task.id ?? UUID()),tag:task.id ?? UUID(),selection: $viewModel.activeId){ VStack(alignment: .leading, spacing: 5, content: { Text(task.content ?? "") .font(.title) .fontWeight(.bold) Text(task.date ?? Date(),style: .date) .fontWeight(.bold) Divider() }) } .foregroundColor(.primary) .contextMenu{ Button(action: { viewModel.EditItem(item: task) }, label: { Text("Edit") }) Button(action: { context.delete(task) try! context.save() }, label: { Text("Delete") }) } } } .padding() }) } } Button(action: {viewModel.isNewData.toggle()}, label: { Image(systemName: "plus") .font(.largeTitle) .foregroundColor(.white) .padding(20) .background(Color.green) .clipShape(Circle()) }) .padding() }) .onOpenURL{ url in viewModel.apply(inputs: .openFromWidget(url: url)) } } .sheet(isPresented: $viewModel.isNewData, content: { NewDataSheet(viewModel: viewModel) }) } } |
Widgetの編集
Widgetに表示されているデータの詳細画面に遷移できるようになりましたが、現在のコードではdate 順で一番上のデータが常に表示されるようになっていますので、Widgetの編集画面で選択したデータをWidgetで表示、詳細画面へ遷移できるように、下記のように編集します。
request.predicate = NSPredicate(format: "id == %@", UUID(uuidString:(configuration.UUID!))! as CVarArg) として、リクエストの条件をidでの一致としています。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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | import WidgetKit import SwiftUI import Intents import CoreData struct Provider: IntentTimelineProvider { var moc = PersistenceController.shared.managedObjectContext init(context : NSManagedObjectContext) { self.moc = context } func placeholder(in context: Context) -> SimpleEntry { var task:Task? let request = NSFetchRequest<Task>(entityName: "Task") request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)] do{ let result = try moc.fetch(request) task = result.first } catch let error as NSError{ print("Could not fetch.\(error.userInfo)") } return SimpleEntry(date: Date(), task: task!, configuration: ConfigurationIntent()) } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { var task:Task? let request = NSFetchRequest<Task>(entityName: "Task") request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)] do{ let result = try moc.fetch(request) task = result.first } catch let error as NSError{ print("Could not fetch.\(error.userInfo)") } let entry = SimpleEntry(date: Date(), task: task!, configuration: configuration) completion(entry) } func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] var task:Task? let request = NSFetchRequest<Task>(entityName: "Task") request.predicate = NSPredicate(format: "id == %@", UUID(uuidString:(configuration.UUID!))! as CVarArg) do{ let result = try moc.fetch(request) task = result.first } catch let error as NSError{ print("Could not fetch.\(error.userInfo)") } // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate, task: task!, configuration: configuration) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } struct SimpleEntry: TimelineEntry { let date: Date let task:Task? let configuration: ConfigurationIntent } struct TestIntentWidgetEntryView : View,TestIntentWidgetType { var entry: Provider.Entry var body: some View { VStack{ Text(entry.date, style: .time) Text(entry.task?.content ?? "") } .widgetURL(makeURLScheme(id: (entry.task?.id)!)) } } @main struct TestIntentWidget: Widget { let kind: String = "TestIntentWidget" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider(context: PersistenceController.shared.managedObjectContext)) { entry in TestIntentWidgetEntryView(entry: entry) .environment(\.managedObjectContext, PersistenceController.shared.managedObjectContext) } .configurationDisplayName("My Widget") .description("This is an example widget.") } } |
次に編集画面の表示を修正します。
現在は、UUIDが表示されており、データの内容がわかりません。
UUIDがDeepLinkで利用するので、この値を保持しながらタスクの内容を表示するために、.intentdefinition を編集します。
TYPESの追加
カスタムTYPESを利用するために新規で作成します。
TYPE名をItemとして作成、その他設定はデフォルトのままです。
ConfigurationのTypeを作成したItemに指定します。
次にこの設定で修正が必要なコードを編集します。
IntentHandler.swift
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 | import WidgetKit import SwiftUI import CoreData import Intents class IntentHandler: INExtension,ConfigurationIntentHandling { var moc = PersistenceController.shared.managedObjectContext func provideUUIDOptionsCollection(for intent: ConfigurationIntent, with completion: @escaping (INObjectCollection<Item>?, Error?) -> Void) { let request = NSFetchRequest<Task>(entityName: "Task") var nameIdentifiers:[Item] = [] do{ let results = try moc.fetch(request) nameIdentifiers = results.map{ Item(identifier:$0.id?.uuidString, display:$0.content!) } } catch let error as NSError{ print("Could not fetch.\(error.userInfo)") } let allNameIdentifiers = INObjectCollection(items: nameIdentifiers) completion(allNameIdentifiers,nil) } override func handler(for intent: INIntent) -> Any { return self } } |
TestIntentWidget.swift
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 | import Foundation //追加 ... func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] var task:Task? let request = NSFetchRequest<Task>(entityName: "Task") request.predicate = NSPredicate(format: "id == %@", UUID(uuidString:(configuration.UUID?.identifier)!)! as CVarArg) do{ let result = try moc.fetch(request) task = result.first } catch let error as NSError{ print("Could not fetch.\(error.userInfo)") } // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate, task: task!, configuration: configuration) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } ... |
これでUUIDを利用しながら、Taskを表示する設定となりました。
お疲れ様でした。
以上、SwiftUIでのWidget_iOS14.0のConfigtation Intentを利用した実装方法、そしてより実践的な利用方法としてCoreDataとの連携方法を紹介しました。