92482


## 前提

Swiftを勉強し始めた初心者です。

[YouTubeの動画](https://www.youtube.com/watch?v=7ZKZCNqR7xI)を参考にメモウィジェットアプリを作成しました。

機能としましては、アプリ内のTextFieldに文字を入力すると、入力した文字がウィジェットに表示されるアプリです。

![イメージ説明](https://ddjkaamml8q8x.cloudfront.net/questions/2022-11-27/fdef552e-52a2-464b-a06e-0c0db416780d.png)  
![イメージ説明](https://ddjkaamml8q8x.cloudfront.net/questions/2022-11-27/dc70524b-787b-42b6-bde6-21e7a96111c6.png)

## 実現したいこと

アプリをタスクキルすると、Widgetに表示されている文字は表示されたままなのですが、アプリ内のTextFieldの文字が消えてしまいます。  
そのため、タスクキルをしてもTextFieldの文字を保持できるようにしたいです。

![イメージ説明](https://ddjkaamml8q8x.cloudfront.net/questions/2022-11-27/d0e7baf3-f4ac-4b55-af0f-35583ca1a9c6.png)  
![イメージ説明](https://ddjkaamml8q8x.cloudfront.net/questions/2022-11-27/01311899-7f4a-4b4c-adf8-ccf49dd35e80.png)

## 試したこと

info.plistに値を追加する方法を試しても解決できませんでした。

[バックグラウンドで動くアプリが終了されたタイミングでアラートを出す](https://koogawa.hateblo.jp/entry/2014/01/21/010008)

## 該当のソースコード

ViewController.swift

[code]

    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var txtText: UITextField!
        override func viewDidLoad() {
            super.viewDidLoad()
            storeData(text: "メモを入力しよう")
        }
    
        @IBAction func btnStoreText(_ sender: Any) {
            storeData(text: txtText.text ?? "--")
        }
        
        func storeData(text : String) {
            let storedata = StoreData(showText: text)
            let primaryData = PrimaryData(storeData: storedata)
            primaryData.encodeData()
        }
    }
    
[/code]

WidgetExtension.swift

[code]

    import WidgetKit
    import SwiftUI
    import Intents
    
    struct Provider: IntentTimelineProvider {
        @AppStorage("CreateWidget", store: UserDefaults(suiteName: "group.isseiueda")) var primaryData : Data = Data()
        func placeholder(in context: Context) -> SimpleEntry {
            let storeData = StoreData(showText: "-")
            return SimpleEntry(storeData : storeData, configuration: ConfigurationIntent())
        }
    
        func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
            guard let storeData = try? JSONDecoder().decode(StoreData.self, from: primaryData) else {
                return
            }
            let entry = SimpleEntry(storeData: storeData, 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)
    //        }
            guard let storeData = try? JSONDecoder().decode(StoreData.self, from: primaryData) else {
                return
            }
            let entry = SimpleEntry(storeData: storeData, configuration: configuration)
            
            let timeline = Timeline(entries: [entry], policy: .never)
            completion(timeline)
        }
    }
    
    struct SimpleEntry: TimelineEntry {
        let date: Date = Date()
        let storeData : StoreData
        let configuration: ConfigurationIntent
    }
    
    struct WidgetExtensionEntryView : View {
        var entry: Provider.Entry
    
        var body: some View {
            Text(entry.storeData.showText)
        }
    }
    
    struct WidgetExtension: Widget {
        let kind: String = "WidgetExtension"
    
        var body: some WidgetConfiguration {
            IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
                WidgetExtensionEntryView(entry: entry)
            }
            .configurationDisplayName("My Widget")
            .description("This is an example widget.")
        }
    }
    
    struct WidgetExtension_Previews: PreviewProvider {
        static let storeData =  StoreData(showText: "-")
        static var previews: some View {
            WidgetExtensionEntryView(entry: SimpleEntry(storeData: storeData, configuration: ConfigurationIntent()))
                .previewContext(WidgetPreviewContext(family: .systemMedium))
        }
    }
    
[/code]

StoreData.swift

[code]

    import Foundation
    
    struct StoreData : Codable {
        var showText : String
    }
    
[/code]

PrimaryData.swift

[code]

    import SwiftUI
    import WidgetKit
    
    struct PrimaryData {
        @AppStorage("CreateWidget", store: UserDefaults(suiteName: "group.isseiueda")) var primaryData : Data = Data()
        let storeData : StoreData
        
        func encodeData() {
            guard let data = try? JSONEncoder().encode(storeData) else {
                return
            }
            primaryData = data
            WidgetCenter.shared.reloadAllTimelines()
        }
        
    }
    
[/code]


CC BY-SA 4.0
2022-11-27T13:12:48.573
92464
2022-11-28T10:33:38.740
2022-11-27T16:29:02.423
3060
55638
question
swift
タスクキル（アプリの再起動）をしてもアプリ内のTextFieldの文字を保持しておきたい


## 問題１(supportedFamiliesがない)

[code]

    struct WidgetExtension: Widget {
      let kind: String = "WidgetExtension"
      
      var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
          WidgetExtensionEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([.systemMedium])
      }
    }
    
[/code]

## 問題2(@AppStorageの使い方が異なる)

getSnapshot,
getTimelineは適切なタイミングでOS側が呼ぶので、@AppStorageで常に監視することはおそらく難しいです。ウィジェットでは

[code]

    struct Provider: IntentTimelineProvider {
      func placeholder(in context: Context) -> SimpleEntry {
        let storeData = StoreData(showText: "placeholder")
        return SimpleEntry(storeData : storeData, configuration: ConfigurationIntent())
      }
      
      func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        guard let primaryData = UserDefaults(suiteName: "group.isseiueda")?.data(forKey: "CreateWidget") else { return }
        
        guard let storeData = try? JSONDecoder().decode(StoreData.self, from: primaryData) else {
          return
        }
        
        let entry = SimpleEntry(storeData: storeData, configuration: configuration)
        completion(entry)
      }
      
      func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        guard let primaryData = UserDefaults(suiteName: "group.isseiueda")?.data(forKey: "CreateWidget") else { return }
        
        guard let storeData = try? JSONDecoder().decode(StoreData.self, from: primaryData) else {
          return
        }
        let entry = SimpleEntry(storeData: storeData, configuration: configuration)
        
        let timeline = Timeline(entries: [entry], policy: .never)
        completion(timeline)
      }
    }
    
[/code]

## 問題3(メイン画面)

UIKitがわからないのでSwiftUIで書きましたが、テキストが変更されるたびに`WidgetCenter.shared.reloadAllTimelines()`を実行することで、ウィジェット側でデータを再取得させています。

[code]

    struct ContentView: View {
      @State var storeData: StoreData
      
      init() {
        guard let data = UserDefaults(suiteName: "group.isseiueda1")?.data(forKey: "CreateWidget") else {
          self.storeData = .init(showText: "text")
          return
        }
        
        if let storeData = try? JSONDecoder().decode(StoreData.self, from: data) {
          self.storeData = storeData
        } else {
          self.storeData = .init(showText: "text")
        }
      }
      
      var body: some View {
        TextField("Text", text: $storeData.showText)
          .onChange(of: storeData) { newValue in
            guard let data = try? JSONEncoder().encode(newValue) else { return }
            UserDefaults(suiteName: "group.isseiueda")?.set(data, forKey: "CreateWidget")
            WidgetCenter.shared.reloadAllTimelines()
          }
      }
    }
    
    struct StoreData : Codable, Equatable {
      var showText : String
    }
    
[/code]


CC BY-SA 4.0
2022-11-28T10:33:38.740
92482
2022-11-28T10:33:38.740
40856
92464
answer