skip to Main Content

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!

3

Answers


  1. Chosen as BEST ANSWER

    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.


  2. 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 CommandShiftK 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.

    Login or Signup to reply.
  3. I’ve had a very similar problem, and I would like to post my solution in here in case someone has the same one, because they will probably end up on this thread like I did.

    My widgets had the feature to add custom images as widget backgrounds, and if the images were too large the widget went totally grey with redacted placeholders (I assume because the timeline provider fails to provide these large images).

    So the solution was to programmatically crop the image to some predefined sizes. I found that for almost every device the max size of 1024 pixels in either width or height was totally fine and working on every iOS version (iOS 16 is the latest version at the time this post was written). Only for iPhone 11 & XR (which have the same screen) the height/width of size 824 pixels was the first value I found that works.

    I wasn’t able to find any documentation regarding those sizes and they may change in future iOS versions.

    If you use static images which are stored in the project Assets, then use the 824 image for the 2x scale, and the 1024 image for the 3x scale.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search