首页 > 解决方案 > iOS 14 小部件在本地工作,但通过 TestFlight 失败

问题描述

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

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

我在 Apple Developer 论坛上看到过一些关于类似问题的帖子。一个被接受的答案是这样说的:

  1. 确保在您的设备上使用 Xcode 12 beta 4 和 iOS 14 beta 4。确保您已实施占位符(in:)。确保您没有 placeholder(with:) 因为这是 Xcode 以前的 beta 建议的自动完成功能,没有它,您将无法使用占位符。我认为这整个问题是由 WidgetKit 方法重命名引起的,但这是另一回事。
  2. 根据发行说明,您需要在扩展目标的构建设置中将“Dead Code Stripping”设置为 NO。这仅对扩展的目标是必需的。
  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中,有一节讨论了后台网络请求:

当您的小部件扩展处于活动状态时,例如在提供快照或时间线时,它可以启动后台网络请求。例如,获取队友当前状态的游戏小部件,或获取带有图像缩略图的标题的新闻小部件。发出异步后台网络请求可让您快速将控制权交还给系统,从而降低因响应时间过长而被终止的风险。

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

标签: iosswiftswiftuiwidgetkit

解决方案


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


推荐阅读