個人開発で得られたWidgetアプリでできないこと

2025年に入って、ウィジェットメインで使用するアプリを3つリリースしました。
本記事の最後にアプリを紹介しています。

本記事では、Widgetの実装で得られた知見について纏めてみました。

# **1. Widgetでできないこと**
Widgetの開発を行っていると、開発する前に「知っておきたかった。」ことや「え〜、それできないんだ。」という発見が結構ありました。
そういった経験を紹介できればと思います。

## 1.1 Widgetは15分に1度の更新のみ
 - 現状だと、**Widgetは15分に1度の更新のみ**ということです。
リアルタイム性のあるものだと勝手に想像していました。
どのくらいの頻度でWidgetを更新するかはgetTimelineの中で指定します。

    <details><summary>getTimelineのコード例</summary>


    ```swift
        func getTimeline(in context: Context, completion: @escaping (Timeline<MarketEntry>) -> Void) {

            // entriesを生成する処理
            
            // タイムラインを更新
            let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date())!
            let timeline = Timeline(entries: entries, policy: .after(nextUpdateDate))

            completion(timeline)
        }
    ```
    </details>
<br>

 - リアルタイム性の求められるアプリを開発していて、当初は更新頻度1分間隔くらいで実装しようと呑気に考えていました…
    <img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128814/6d5d4c8b-e0d6-4774-bedc-8393986d1ffb.jpeg">

## 1.2 WidgetはUIPasteboardにアクセスできない(コピペが使用できない)
 - Widgetでテキストをコピーできたら便利だなと考え、実装してみました。
 
    WidgetでUIPasteboardにアクセスできないのであれば、「はい、そうですか。」で終わりなのですが、シミュレーターで動作確認するとUIPasteboardにアクセスすることができるため、無事コピーできると勘違いしてしまうのです。
    
    そして実機で確認すると全く動作しません。
    どうにかコピーできないかと試行錯誤してみますが、動作はしません。
    
    調べてみると同じようなことを言っている人がいます。
    https://stackoverflow.com/questions/78670089/cant-use-uipasteboard-on-widget-swift-on-real-device
        <img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128814/51f2b50d-6375-443a-8b18-30609cb2dab4.png">

<br>

- この件について、解消方法が存在するのであれば教えていただきたいです。
また、この罠に引っ掛かる人が減れば幸いです。

## 1.3 Widgetでは`UserDefaults.standard`からのデータを参照できない
- アプリ側で`UserDefaults.standard`使用して保存した変数をWidget側で参照したいというケースがあると思います。
 Widgetでは、[**App Groups**](https://developer.apple.com/documentation/xcode/configuring-app-groups)で設定したIDの`UserDefaults`を使用する必要があります。
 App Groupsの`UserDefaults`を用意したら、使用方法は`UserDefaults.standard`と同じです。
 App GroupsはSigning & Capabilitiesから設定できます。

    ```swift
    let sharedDefaults = UserDefaults(suiteName: "group.com.xxxx.xxxxx")
    ```

## 1.4 `UIApplication.shared`はWidgetでは使用できない
- 前提として、Widgetで`UIApplication.shared`を使用することはないです。
使用すると、以下のエラーが発生します。
    ```
    'shared' is unavailable in application extensions for iOS: Use view controller based solutions where appropriate instead.
    ```
<br>

-   **そもそも、なぜこのエラーが発生したのか？**
    次のコードのように、アプリ側とWidget側で処理を共通化したUtilというファイルを使っていました。
    つまり、UtilファイルのTargetにはアプリとWidgeExtensionを指定している状態ということになります。
    UIApplication.sharedはWidgetでは使用できないので、当然ビルドエラーとなり上記のエラーが発生します。
- 解消方法
    - アプリ側とWidget側で共通化処理のファイルを別々で用意する
    - アプリ側とWidget側で両方とも使用する場合は、コメントアウト等で運用方法を記載する
   ```swift
   struct Util {
        static let windowScene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene
    }
    ```
## 1.5 Widgetで`ScrollView`は使用できない
ScrollViewを使用するとこうなります(笑)
  <img width="240" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128814/192e7e67-8648-4bfb-80f4-3e6eeaa2c757.png">

# **2. その他**
## 2.1 インタラクティブなWidgetを作成する
- Widgetでは、ボタンを使用することができます。
    <details><summary>AppIntentに準拠したStructを作成して、Buttonで使用する</summary>

    ```swift
    struct EqualIntent: AppIntent {
        static var title: LocalizedStringResource = "Equal"
        static var description = IntentDescription("Equal the display")
        
        func perform() async throws -> some IntentResult {
            CalculatorState.tappedEqual()
            return .result()
        }
    }

    // ボタンの呼び出し元
    Button(intent: EqualIntent()) {
        Text("=")
            .font(.system(size: 24, weight: .bold))
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .foregroundColor(.white)
    }
    .background(RoundedRectangle(cornerRadius: buttonNumberRadius).foregroundStyle(.orange))
    ```
    </details>
    
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128814/e2e10b09-7b45-4b58-8bec-7db362b118ae.png">


## 2.2 WidgetのButtonのBackgroundを消す方法
- WidgetでButtonを使用すると、自動で青色のBackgroundが付きます。
ButtonStyleを付与する必要がある。普通にButtonにModifierを付与するのでは、解消できません。

  <details><summary>ButtonStyleのコード例</summary>
  
  ```swift
    struct ClearBackgroundStyle: ButtonStyle {
        func makeBody(configuration: Configuration) -> some View {
            configuration.label
                .background(Color.clear)
        }
    }
   ```
</details>


# **まとめ** 
もっと重要なことがあった気がしたのですが、忘れちゃいました(笑)
今後は発見し次第、どこかに纏めておこうと思います。

以下、リリースしたアプリです。
海外の株式市場にズブズブに浸かっている人は、MarketTimeZenを使ってみて欲しいです。

## **1. Reawake** 
iPhoneからMacBookの電池残量を確認できるアプリ。
(リリースから修正していないので、動作が怪しいかも…)

https://apps.apple.com/jp/app/reawake/id6742046854

## **2. MarketTimeZen** 
タイムゾーンに合わせて株式市場がいつ開くのかをロック画面やウィジェットから確認できる。
海外に行った際に「市場もう始まってたのか！」であったり、
開場する時を楽しみに待っていたが、「今日アメリカは祝日で閉場してたのかい…」という経験を解消するために開発しました。

https://apps.apple.com/jp/app/markettimezen/id6743628922

## **3. Ex Rate Calc - ウィジェットで為替電卓 -**
シンプルな為替の電卓です。
例えばアメリカに行って何かを買うときにドルをそのまま入力して円換算できたら便利かもな？と思い開発しました。

https://apps.apple.com/jp/app/ex-rate-calc-%E3%82%A6%E3%82%A3%E3%82%B8%E3%82%A7%E3%83%83%E3%83%88%E3%81%A7%E7%82%BA%E6%9B%BF%E9%9B%BB%E5%8D%93/id6744843796