iOS 14 widget works locally, but fails via TestFlight
Asked Answered
F

3

11

I have a SwiftUI app with a widget. When I run the app via Xcode (either straight to my device or on the simulator), the widget works exactly as expected.

However, when I run the app through TestFlight, the widget does appear, but it does not show any data -- it's just the empty placeholder. The widget is supposed to show an image and some text, but it shows neither.

I've seen some posts on Apple Developer forums about similar problems. One accepted answer says the following:

  1. Make sure that you use Xcode 12 beta 4 and iOS 14 beta 4 on your devices. Make sure that you have placeholder(in:) implemented. Make sure that you don't have placeholder(with:) because that's what the previous beta of Xcode was suggesting with autocompletion and without that you won't get your placeholder working. I think this whole problem is caused by the WidgetKit methods getting renamed but that's another story.
  2. As per the release notes, you need to set "Dead Code Stripping" to NO in your extension target's build settings. This is only necessary for the extension's target.
  3. When uploading your archive to the App Store Connect, uncheck "Include bitcode for iOS content".
  4. Delete your old build from a device when installing a new beta.

I've implemented these suggestions, to no avail.

Here's my code for the widget. It first fetches game data via CloudKit, then creates a timeline:

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])
    }
}

Question: Why isn't my widget working when distributed through TestFlight? What are my options, here?

Thank you!

Update: If I use the unredacted() view modifier, the widget shows the image and the "LEVEL" and "SCORE" text, but still does not show any actual data. So, my SwiftUI view now looks like this:

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
        }
    }
}

Update #2: In the article Keeping A Widget Up To Date, there's a section that talks about background network requests:

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.

Do I need to set up this (complicated) background request paradigm in order to make CloudKit work for my widget? Am I on the right track?

Fearnought answered 11/5, 2021 at 7:37 Comment(9)
developer.apple.com/forums/thread/653112Predial
@loremipsum Unfortunately, nothing in that thread solves my problem. I've already added the iCloud Capability to my widget target, and I'm not using CoreData. Thank you, though!Fearnought
NP I figured some of the CloudKit stuff might work like making sure iCloud is enabled, it was in the responses section not necessarily the questionPredial
Facing the exact same issue. Can’t figure out why it runs fine on simulator and debug build locally but is completely missing when I try through TestFlight. I did the same things suggested in the dev forums but that look like old suggestions.Weaver
@Weaver Just a thought, here: Widgets will only appear after you've opened the corresponding app once. Are you opening the main app before looking for the widget?Fearnought
@Fearnought Thank you. I was opening the app first. I actually finally got it to work. I discovered the issue was one of the external libraries the project used. I was able to remove the library from being included in widgetkit and it started working. Throughout I never got any build or linking errors so couldn’t figure out what was going on.Weaver
Thanks for the info, @Prasanth. Glad you got it working!Fearnought
One more suggestion, to make it close to TestFlight build, go to your current Scheme > Edit Scheme > Run and change Build Configuration from Debug to Release, so it will be exactly the same configuration as a release build.Arthro
@Arthro I'm getting the same result with a "Release" build, unfortunately. So, it's definitely not working for Release, which is good to know.Fearnought
N
2

Did you try to deploy the cloudkit container to the production? You can find it on the CloudKit Dashboard.

Nmr answered 22/5, 2021 at 18:23 Comment(0)
A
3

I was stuck with a similar problem (but not using CloudKit). Just in case this helps anyone else, my problem was that I was using code like this to get my app group to communicate between WidgetExtension and main target.

#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

However, these preprocessor definitions only existed on the main target and not the widget extension, so I was using mismatched app groups. Moving the preprocessor definitions to the project file level fixed it.

Specifically, I did not need to do anything with background URL sessions, which I was also wondering about.

Anishaaniso answered 25/1, 2022 at 23:19 Comment(3)
For me, it was not related to app groups, but it was a preprocessor macro, i.e. a flag (DEBUG) that wasn't set for the TestFlight (release) build configuration naturally. Took me hours to figure this out, thanks for the hint!Ursine
@Ursine what flag was it?Katy
enable_firebase. It most likely won't help you. But maybe you have some sort of a condition that enables something (e.g. a framework) on you local DEV build environment, but not on your TestFlight build environment (e.g. STAGING or something like that).Ursine
N
2

Did you try to deploy the cloudkit container to the production? You can find it on the CloudKit Dashboard.

Nmr answered 22/5, 2021 at 18:23 Comment(0)
C
0

With iOS 17 it has happened the same to me but widgets start working after rebooting the iPhone

Cachou answered 31/3 at 9:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.