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:
But after some time and after some more iPhone unlocks BOOM, I get this:
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 TimelineProvider
s 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
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.
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, with460 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.
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: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.
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.