iOS 14 个小部件在本地工作,但通过 TestFlight 失败

iOS 14 widget works locally, but fails via TestFlight

我有一个带小部件的 SwiftUI 应用程序。当我通过 Xcode 运行 应用程序(直接在我的设备或模拟器上)时,小部件完全按预期工作。

但是,当我通过 TestFlight 运行 应用程序时,小部件确实出现了,但它不显示任何数据 -- 它只是一个空的占位符。该小部件本应显示图像和一些文本,但两者均未显示。

我在 Apple Developer 论坛上看到过一些关于类似问题的帖子。一个接受 answer 说如下:

  1. 确保您在设备上使用 Xcode 12 beta 4 和 iOS 14 beta 4。确保你有 placeholder(in:) 实现。确保你没有 placeholder(with:) 因为这是 Xcode 的先前测试版建议自动完成的内容,没有它你将无法让你的占位符工作。我认为这整个问题是由重命名的 WidgetKit 方法引起的,但那是另一回事了。
  2. 根据发行说明,您需要在扩展目标的构建设置中将“死代码剥离”设置为“否”。这仅是扩展目标所必需的。
  3. 将存档上传到 App Store Connect 时,取消选中“包含 iOS 内容的位码”。
  4. 安装新测试版时从设备中删除旧版本。

我已实施这些建议,但无济于事。

这是我的小部件代码。它首先通过 CloudKit 获取游戏数据,然后创建时间线:

import WidgetKit
import SwiftUI
import CloudKit

struct WidgetCloudKit {
    static var gameLevel: Int = 0
    static var gameScore: String = ""
}


struct Provider: TimelineProvider {
    private var container = CKContainer(identifier: "MyIdentifier")
    static var hasFetchedGameStatus: Bool = false
    

    func placeholder(in context: Context) -> SimpleEntry {
        return SimpleEntry(date: Date(), gameLevel: 0, gameScore: "0")
    }

    
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry: SimpleEntry

        if context.isPreview && !Provider.hasFetchedGameStatus {
            entry = SimpleEntry(date: Date(), gameLevel: 0, gameScore: "0")
        } else {
            entry = SimpleEntry(date: Date(), gameLevel: WidgetCloudKit.gameLevel, gameScore: WidgetCloudKit.gameScore)
        }
        completion(entry)
    }


    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
            let pred = NSPredicate(value: true)
            let sort = NSSortDescriptor(key: "creationDate", ascending: false)
            let q = CKQuery(recordType: "gameData", predicate: pred)
            q.sortDescriptors = [sort]

            let operation = CKQueryOperation(query: q)
            operation.desiredKeys = ["level", "score"]
            operation.resultsLimit = 1

            operation.recordFetchedBlock = { record in
                DispatchQueue.main.async {
                    WidgetCloudKit.gameLevel = record.value(forKey: "level") as? Int ?? 0
                    WidgetCloudKit.gameScore = String(record.value(forKey: "score") as? Int ?? 0)
                    Provider.hasFetchedGameStatus = true

                    var entries: [SimpleEntry] = []
                    let date = Date()

                    let entry = SimpleEntry(date: date, gameLevel: WidgetCloudKit.gameLevel, gameScore: WidgetCloudKit.gameScore)
                    entries.append(entry)

                    // Create a date that's 15 minutes in the future.
                    let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 15, to: date)!
                    let timeline = Timeline(entries: entries, policy: .after(nextUpdateDate))
                    completion(timeline)
                }
            }

            operation.queryCompletionBlock = { (cursor, error) in
                DispatchQueue.main.async {
                    if let error = error {
                        print("queryCompletion error: \(error)")
                    } else {
                        if let cursor = cursor {
                            print("cursor: \(cursor)")
                        }
                    }
                }
            }
                    
            self.container.publicCloudDatabase.add(operation)
    }
    
}

struct SimpleEntry: TimelineEntry {
    var date: Date
    var gameLevel: Int
    var gameScore: String
}

struct WidgetEntryView : View {
    var entry: Provider.Entry
    
    var body: some View {
        GeometryReader { geo in
            VStack {
                Image("widgetImage")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: geo.size.width)
                HStack {
                    VStack {
                        Text("LEVEL")
                        Text(entry.gameLevel == 0 ? "-" : "\(entry.gameLevel)")
                    }
                    VStack {
                        Text("SCORE")
                        Text(entry.gameScore == "0" ? "-" : entry.gameScore)
                    }
                }
            }
        }
    }
}

@main
struct Widget: SwiftUI.Widget { 
    let kind: String = "MyWidget"
    
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            WidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Game Status")
        .description("Shows an overview of your game status")
        .supportedFamilies([.systemSmall])
    }
}

问题:为什么我的小部件在通过 TestFlight 分发时不工作?在这里我有什么选择?

谢谢!

更新: 如果我使用 unredacted() 视图修饰符,小部件会显示图像以及“LEVEL”和“SCORE”文本,但仍不显示任何实际数据。所以,我的 SwiftUI 视图现在看起来像这样:

struct WidgetEntryView : View {
    var entry: Provider.Entry
    
    var body: some View {
        GeometryReader { geo in
            VStack {
                Image("widgetImage")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: geo.size.width)
                HStack {
                    VStack {
                        Text("LEVEL")
                        Text(entry.gameLevel == 0 ? "-" : "\(entry.gameLevel)")
                    }
                    VStack {
                        Text("SCORE")
                        Text(entry.gameScore == "0" ? "-" : entry.gameScore)
                    }
                }
            }
                .unredacted() // <-- I added this
        }
    }
}

更新#2: 在文章Keeping A Widget Up To Date中,有一节讲到后台网络请求:

When your widget extension is active, like when providing a snapshot or timeline, it can initiate background network requests. For example, a game widget that fetches your teammate’s current status, or a news widget that fetches headlines with image thumbnails. Making asynchronous background network requests let you return control to the system quickly, reducing the risk of being terminated for taking too long to respond.

我是否需要设置这个(复杂的)后台请求范例才能让 CloudKit 为我的小部件工作?我在正确的轨道上吗?

您是否尝试将 cloudkit 容器部署到生产环境中?您可以在 CloudKit 仪表板上找到它。

我遇到了类似的问题(但没有使用 CloudKit)。以防万一这对其他人有帮助,我的问题是我使用这样的代码让我的应用程序组在 WidgetExtension 和主要目标之间进行通信。

#if DEBUG
    static let appGroup = "group.com.myapp.debug"
#elseif BETA
    static let appGroup = "group.com.myapp.beta"
#else
    static let appGroup = "group.com.myapp"
#endif

但是,这些预处理器定义仅存在于主要目标上,而不存在于小部件扩展中,因此我使用的是不匹配的应用程序组。将预处理器定义移动到项目文件级别修复了它。

具体来说,我 不需要 需要对背景 URL 会话做任何事情,我也想知道这一点。