【iOS】ウィジェットとCoreDataを連携する入門〜ウィジェットから特定の画面に遷移〜

2023.03.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

前回は、【iOS】ウィジェットとCoreDataを連携する入門〜ユーザーが構成可能なウィジェットを作成する〜という記事を書いたので、次はさらなる続編として、ウィジットから特定の画面に遷移する方法を記載したいと思います。

はじめに

下記の記事の続編になりますので、不明点があれば下記を参照していただければと思います。

【iOS】ウィジェットとCoreDataを連携する入門〜ユーザーが構成可能なウィジェットを作成する〜

作ったもの

coredata-intent-present

環境

  • Xcode 14.2
  • iOS 16.2

選択されたウィジェットを識別できるようにURLをセットする

widgetURL(_:)モディファイアを使用して、タップされたウィジェットを特定する為のURLをセットしています。

今回はURL(string: "example://widgetlink?item_id=\(intentItem.identifier ?? "")")でクエリアイテムとしてItemidをセットしています。

struct CoreDataWidgetEntryView : View {

    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>

    let entry: Provider.Entry

    var body: some View {

        // 選択されたアイテムがある場合は表示
        if let intentItem = entry.configuration.parameter {

            VStack {
                Text("選択中")
                Text(intentItem.displayString)
                // 追加
                // widgetのURLをアプリ側でハンドリングできるようにする
                    .widgetURL(URL(string: "example://widgetlink?item_id=\(intentItem.identifier ?? "")"))
            }

        } else {
            VStack {
                Rectangle()
                    .fill(.blue.gradient)
                    .frame(height: 32)
                Divider()

                Spacer()

                ForEach(items) { item in
                    Text(item.timestamp ?? Date(), style: .time)
                }

                Spacer()
            }
        }
    }
}

リンクを設定する方法は、2通りあります。上述したwidgetURL(_:)を使用する方法と、下記のようにLinkを使用する方法です。

Link(destination: URL(string: "example://widgetlink?item_id=\(intentItem.identifier ?? "")")!,
     label: { Text(intentItem.displayString) })

ウィジェットサイズやリンクの違いについては[iOS 14]WidgetでDeep Link作成の中で詳しく書いてありましたので、こちらを確認いただければと思います。

アプリ側でURLをハンドリングする

URLからアイテムのIDを取得

今回のウィジェットから送られてくるURLは、example://widgetlink?item_id=のような形式になる為、該当のURLならば、クエリパラメータのUUIDを取得します。

private func getItemId(from url: URL) -> UUID? {
    guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true),
          urlComponents.scheme == "example",
          urlComponents.host == "widgetlink",
          urlComponents.queryItems?.first?.name == "item_id",
          let uuidString = urlComponents.queryItems?.first?.value else {
        return nil
    }
    return UUID(uuidString: uuidString)
}

該当IDのアイテムをCoreDataから取得

UUIDが一致するアイテムを取得します。

private func fetchItem(id uuid: UUID) -> Item? {
    let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
    fetchRequest.predicate = NSPredicate(format: "id == %@", uuid as CVarArg)

    do {
        let items = try viewContext.fetch(fetchRequest)
        guard let item = items.first
        else { return nil }
        return item

    } catch let error as NSError {
        let nsError = error as NSError
        fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
    }
}

ContentViewで画面を表示させる

import SwiftUI
import CoreData
import WidgetKit

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults

    // 表示する為のアイテム
    @State private var selectedItemForPresenting: Item?

    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        Text("Item at \(item.timestamp!, formatter: itemFormatter)")
                    } label: {
                        Text(item.timestamp!, formatter: itemFormatter)
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
            Text("Select an item")
        }
        // URLのハンドリング
        .onOpenURL { url in
            let uuid = getItemId(from: url)
            if let uuid,
               let item = fetchItem(id: uuid) {
                selectedItemForPresenting = item
            }
        }
        // 表示する為のアイテムがある場合は、sheetで表示
        .sheet(item: $selectedItemForPresenting) { item in
            Text("Item at \(item.timestamp!, formatter: itemFormatter)")
        }
    }

// ~以下省略

.opneURL

ウィジェットをタップして、リンクのURLが存在する場合は、onOpenURL内の処理が実行されます。

// URLのハンドリング
.onOpenURL { url in
    let uuid = getItemId(from: url)
    if let uuid,
       let item = fetchItem(id: uuid) {
        selectedItemForPresenting = item
    }
}

URLからUUIDを取得して、そのUUIDと一致するアイテムを取得して、@State変数のselectedItemForPresentingに値を代入しています。

.sheet(item:onDismiss:content:)

今回でいうと、@State変数のselectedItemForPresentingに値がある場合に、 モーダルで画面を表示するようにしています。

// 表示する為のアイテムがある場合は、sheetで表示
.sheet(item: $selectedItemForPresenting) { item in
    Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}

以上で、ウィジェットを選択して特定の画面に遷移するところまでが出来ました。

おわりに

触れば触るほど、ウィジェットで何か楽しそうなことが出来そう気がムンムンします。

mediumlargeサイズのウィジットでは複数のリンクを設置することも出来るので、ウィジットだけで何か楽しいアプリが出来るのでは?

ワクワクは広がるばかり。これからもアプリ開発を楽しんでいきたいと思います。

参考