【iOS】極力アプリの滞在時間を減らしたい。ウィジェット完結型アプリへの挑戦
最近、WidgetKit周りを触っていて、「これ、ウィジェット自体が一つのミニアプリとして成立していて、アプリに遷移することなくちょっとした操作もウィジェットで完結したら面白いのでは?」という興味が湧いてきました。
TODO管理アプリくらいならイメージしていることは出来そうと思い、ウィジェット完結型アプリ作りにチャレンジしてみることにしました。
作ったもの
WidgetTodo
極力アプリでの滞在時間を減らし、ウィジェット上からタスク完了を行えるTODO管理アプリです。アプリを開くのも面倒くさい、すぐにTODOを確認したい、完了にしたいという方の為に作成しました。

App Storeからインストール出来るので是非どこかに置いてあげて下さい
環境
- Xcode 14.2
- iOS 16.2
はじめに
全体のコードの説明やWidgetKitの説明については省略させていただきます。以前にすでにいくつか記事を書いており、そちらを見ていただければと思います。
説明
ウィジェット上で特定のボタンが押されたことを検知する
複数のリンクを設置できるサイズ
複数のWidgetFamilyがありますが、下記3つのウィジェットサイズでは複数のLinkを設置することが出来ます。
- systemMedium
- systemLarge
- systemExtraLarge
WidgetLinkHandler
ウィジェットのリンクを作成したり、リンクから特定のIDを取得するモデルです。
class WidgetLinkHandler {
    
    private let scheme = "widget-todo"
    private let hostName = "task_completed"
    private let queryItemName = "item_id"
    
    func createCompletedTaskUrl(with id: UUID?) -> URL {
        guard let id else {
            fatalError("Failure to create url due to UUID is nill")
        }
        return URL(string: "\(scheme)://\(hostName)?\(queryItemName)=\(id.uuidString)")!
    }
    
    func getCompletedTaskItemId(from url: URL) -> UUID? {
        
        guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true),
              urlComponents.scheme == scheme,
              urlComponents.host == hostName,
              urlComponents.queryItems?.first?.name == queryItemName,
              let uuidString = urlComponents.queryItems?.first?.value else {
            return nil
        }
        return UUID(uuidString: uuidString)
    }
}
複数のリンクを設置
今回は、ForEachで各itemを取得して、itemの数だけLink(destination:)を設置しています。
import SwiftUI
struct WidgetTodoListView: View {
    
    let items: [Item]
    private let linkHandler = WidgetLinkHandler()
    
    var body: some View {
        
        ForEach(items) { item in
            
            VStack(spacing: 8) {
                
                HStack(spacing: 16) {
                    Spacer()
                        .frame(width: 8)
                    
                    Link(destination: linkHandler.createTaskCompletedUrl(with: item.id)) {
                        Image(systemName: "circle")
                            .font(.system(size: 24))
                            .foregroundColor(.gray)
                    }
                    
                    Text(item.title ?? "")
                    
                    Spacer()
                }
                
                Divider()
                    .padding(.leading)
            }
        }
        
        Spacer()
    }
}
押したリンクをアプリ側で検知する
onOpenURLモディファイアを使用することで、アプリ起動時にリンクを受け取ることが出来ます。
ContentView()
    .onOpenURL { url in
        handleUrl(url)
    }
受け取ったリンクから、どのIDのリンクが押されたかを検知して、そのIDに対して行いたい処理を実行します。
private func handleUrl(_ url: URL) {
    guard let id = linkHandler.getCompletedTaskItemId(from: url) else {
        return
    }
    // Do something you want
}
ホーム画面に戻る
今回のアプリの大事な要素であるデバイスのホーム画面に戻る処理です。
UIApplication.sharedに対して、URLSessionTask.suspendを送ることでアプリをsuspend状態にさせることが出来ます。
import UIKit
extension UIControl {
    
    static func backToHomeScreenOfDevice() {
        UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
    }
}
後は、リンクを検知して行いたい処理を実施後、ホーム画面に戻させれば完了です。
private func handleUrl(_ url: URL) {
    guard let id = linkHandler.getCompletedTaskItemId(from: url) else {
        return
    }
    // Do something you want
+   UIControl.backToHomeScreenOfDevice()
}
ウィジェットがインストールされているか判定する
今回はウィジェットをインストールしてもらわないと意味が無いので、ウィジェットのインストール状態を判定して、未インストールの場合はウィジェットの追加方法を表示するようにしています。
おわりに
今回はかなり実験的な試みをしてみました。
ウィジェット上のボタンについては、HIGの方針を守れていなかったり、ウィジェット上からタスク完了をした際に一瞬だけどアプリがフォアグラウンドになる挙動が少し気持ち悪かったりしますが、全てはユーザー様のひと手間を無くす為の結果になります。多めにみてあげて下さい。
ストア審査で引っかかるかな?と思っていたのですが、心の広いApple様は弊アプリを受け入れてくれました。
今回はキーボード入力は諦めてアプリ側で入力してもらっているのですが、よく考えてみるとキーボード入力もウィジェット完結型出来そうなので気が向いたら作ってみようと思います。
あと、ウィジェット版たまごっち的なものも作れそうと思ったり。
是非、皆さんもウィジェットで遊んでみて下さい☺︎

参考




Discussion
大変参考になります。
アプリを実際にダウンロードして使ってみたら、一瞬もフォアグランドにならずに更新されました。
どのように実装したのでしょうか。
教えていただけると嬉しいです。
現在では、公式が提供している方法でウィジェットを更新しております。
実装方法については、こちらの記事がとてもわかりやすいので、こちらの記事を参考にすると良さそうです!
早々にご返信ありがとうございます。
実装できました。