SwiftUI iOS Widget – Image after some time gets cropped

Viewed 191

I am creating iOS 14+ Widget using SwiftUI and I'm facing very strange situation. Problem is that I have just one "full widget" image (maybe or not its worth noting that it's downloaded from the internet but in the time of displaying its already loaded and injected as UIImage) in my widget which is displayed correctly like that:

enter image description here

But after some time and after some more iPhone unlocks BOOM, I get this:

enter image description here

Few observations:

  • it happens with systemLarge family as well - the same percentage of image's width is cropped
  • the image is cropped horizontally always by exactly the same percentage of width
  • when my wallpaper is other than just plain black the cropped part is filled with black color as well
  • the cropped part is filled with black no matter what user interface style (light/dark) I have

To make it easier I created minimal example which causes the error:

So, I have this EntryView

struct EntryView: View {

    var viewModel: EntryViewModel

    var body: some View {
        Image(uiImage: image)
            .resizable()
            .scaledToFill()
    }
}

which is used as content for Widget itself

struct SomeWidget: Widget {

    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: xxx,
            provider: SomeWidgetTimelineProvider(xxx)
        ) { item in
            view(for: item)
        }
            .configurationDisplayName(xxx)
            .description(xxx)
            .supportedFamilies([.systemMedium, .systemLarge])
    }

    private func view(for item: Item) -> some View {
        Group {
            switch item {
            case .aaa(let xxx):
                EntryView(viewModel: xxx)
            case .bbb(let xxx):
                EntryView(viewModel: xxx)
            }
        }
    }
}

and this Widget is wrapped in WidgetBundle

@main
struct SomeWidgets: WidgetBundle {

    var body: some Widget {
        SomeWidget()
        SomeWidget()
    }
}

Maybe I should also show logic which is responsible for downloading the image. I use just simple Data(contentsOf:) synchronous method which is called on background queue and then I call TimelineProviders callback on main queue:

final class SomeWidgetTimelineProvider: TimelineProvider {

    ...

    func getTimeline(in context: Context, completion: @escaping (Timeline<SomeEntry>) -> Void) {
        getSomeModel { model in
            prefetchImage(for: model) { result in
                switch result {
                case .success(let image):
                    completion(
                        Timeline(
                            entries: [.aaa(EntryViewModel(image: image)],
                            policy: .after(Date() + 30 * 60)
                        )
                    )
                case .error(let error):
                    completion(
                        Timeline(
                            entries: [.bbb(xxx)],
                            policy: .after(Date() + 30 * 60)
                        )
                    )
                }
            }
        }
    }

    private func prefetchImage(for model: SomeModel, completion: @escaping (Result<UIImage, Error>) -> Void) {
        DispatchQueue.global(qos: .background).async {
            guard
                let imageURL = URL(string: model.imageUrl),
                let data = try? Data(contentsOf: imageURL)
            else {
                DispatchQueue.main.async {
                    completion(.failure(xxx))
                }
            }
            let image = UIImage(data: data)
            DispatchQueue.main.async {
                completion(.success(image))
            }
        }
    }
}

So my question is, is something wrong with my layout or fetching logic? What am I missing?

Thank you for any help!

2 Answers

Several issues are possible.

1. Resolution issue

The problem often stems from your widget's image resolution. The resolution of the image in pixels must correlate with the Widget's resolution in points. In the following pivot table, you can see what resolution in points (pts) some of the iPhone models use. Points are different to pixels because they change size based on PPI. 1 point is equal to 1 pixel when the resolution is 163 PPI. This was the case for all the iPhones before the Retina era. Today, however, with 460 PPI, the iPhone 12 Pro's 1 point is equal to 3 pixels across and 3 pixels down, or 9 total pixels.

Thus, your image size for the Medium Widget should not exceed 1092 x 510 pixels (72 pixels/inch) for iPhone 13 Pro Max. Using 4K source images for iOS Widgets is highly inappropriate.


Model Screen size (pts) Small widget (pts) Medium widget (pts) Large widget (pts)
iPhone 13 Pro Max 428 x 926 170 x 170 364 x 170 364 x 382
iPhone 11 414 x 896 169 x 169 360 x 169 360 x 379
iPhone 8 Plus 414 x 736 159 x 159 348 x 157 348 x 357
iPhone 12 Pro 390 x 844 158 x 158 338 x 158 338 x 354
iPhone X 375 x 812 155 x 155 329 x 155 329 x 345
iPhone 7 375 x 667 148 x 148 321 x 148 321 x 324
iPhone SE 320 x 568 141 x 141 292 x 141 292 x 311

2. File format issue

Widgets images must be in 8-bit .png format. Images may include an alpha channel but should not include any transparent regions (i.e. alpha channel should be 100% white).

3. DispatchQoS.QoSClass

Use quality-of-service with the highest priority, i.e. .userInteractive case:

DispatchQueue.global(qos: .userInteractive).async { ... }

4. Invalid file issue

Try using a different .png. It is quite possible that the image you downloaded from the Internet is damaged or corrupted. An image may be corrupted due to interruption of transfer, malware/virus infection, SSD bad blocks, etc.

5. Cleanup

Sometimes, you have to delete/clean builds if you observe an improper functioning, indexing or slowness. If so, delete your project's build from the ~/Library/Developer/ folder. Next, in Xcode, select project's icon in Navigator panel and press Command-Shift-K shortcut to clean the build.

Then, close Xcode, go to ~/Library/Developer/Xcode/DerivedData/ModuleCache directory and delete the cache.

6. Xcode version

Run your project on the release version of Xcode 14. Do not use betas.

Just to let anyone know, Andy's answer has good points. Anyway, for me it seems that the problem does no longer exist on iOS 16. So maybe it was a bug in OS itself because without any changes in code the same code works on iOS 16 while not on iOS 15.

Related