Tuesday, April 16, 2024
HomeProgrammingSwiftUI Lists Are Damaged And Can’t Be Fastened | by Michael Lengthy...

SwiftUI Lists Are Damaged And Can’t Be Fastened | by Michael Lengthy | Sep, 2022


How an age-old drawback has surfaced as soon as extra.

Picture by Chris Lawton on Unsplash

So over the weekend, I used to be engaged on the SwiftUI demo app for RequestBuilder and I bumped into an issue.

RequestBuilder, because the identify may point out, makes use of the Builder design sample to construct and execute URLRequests for Mix and Async/Await. The demo app makes use of it to fetch a listing of customers from an API, together with their thumbnails for presentation in a SwiftUI checklist.

Within the utility, the thumbnails are fetched from a devoted service that caches the pictures for later use. To date, so good.

The app had labored fantastic earlier than, however now once I ran it and scrolled down I started to see customers within the checklist that have been solely exhibiting the placeholder picture, and never their precise thumbnail.

Evidently, I used to be not happy, and I spent the following couple of hours attempting to determine what was flawed with my library.

Solely to find that it wasn’t my library in any respect.

The wrongdoer appeared to be SwiftUI itself.

As you may be capable to deduce from the screenshot exhibiting the brand new “Dynamic Island”, I used to be working iOS 16 on an iPhone 14 simulator utilizing Xcode 14.0.1, a brand new model of Xcode I’d simply put in.

Hmmm.

I’d simply run the venture the day earlier than, however had completed so utilizing Xcode 13.3.1 and with a simulator working iOS 15.6… and SwiftUI 3.

Right here’s the code exhibiting the cardboard view within the checklist.

struct MainListCardView: View {  let consumer: Person  @State personal var picture: UIImage?  personal let photos = Container.userImageCache()  var physique: some View {
let _ = Self._printChanges()
HStack(spacing: 12) {
ZStack {
if let picture = picture {
Picture(uiImage: picture)
.resizable()
.aspectRatio(contentMode: .match)
} else {
Picture("Person-Unknown")
.resizable()
.aspectRatio(contentMode: .match)
.onReceive(photos.thumbnail(forUser: consumer)) {
picture = $0
}
}
}
.body(width: 50, top: 50)
.clipShape(Circle())
...

Container.userImageCache is a service supplied by Manufacturing facility, my dependency injection library. The suspicious code is within the onReceive handler, the place we ask the service for a thumbnail and it’ll both return a cached picture or fetch one from the API.

When it receives a picture from the service it locations it right into a State object, which modifications the state and triggers a refresh, which shows the brand new picture. Easy and easy… however it was not working.

Let’s work the issue individuals

I added some debugging code to the handler.

.onReceive(photos.requestThumbnail(forUser: consumer)) {
print("onReceive (consumer.image?.thumbnail)")
picture = $0
}

And added some data to the checklist show code itself so I may see what picture fetch requests have been failing

Right here’s a pattern of the log.

MainListCardView: _photo modified.
MainListCardView: @self, @id, _photo modified.
REQ: https://randomuser.me/api/portraits/thumb/males/6.jpg
MainListCardView: @self, @id, _photo modified.
MainListCardView: @self, @id, _photo modified.
200: https://randomuser.me/api/portraits/thumb/males/6.jpg
onReceive "https://randomuser.me/api/portraits/thumb/males/6.jpg"
MainListCardView: _photo modified.
MainListCardView: @self, @id, _photo modified.
MainListCardView: @self, @id, _photo modified.
MainListCardView: @self, @id, _photo modified.
MainListCardView: @self, @id, _photo modified.
MainListCardView: @self, @id, _photo modified.
MainListCardView: @self, @id, _photo modified.
MainListCardView: @self, @id, _photo modified.
MainListCardView: @self, @id, _photo modified.
MainListCardView: @self, @id, _photo modified.
MainListCardView: @self, @id, _photo modified.
MainListCardView: @self, @id, _photo modified.

As you’ll be able to see, after some time the onReceive handler itself was merely not being referred to as.

I attempted shifting the situation of onReceive handler to totally different areas within the view simply to see what would occur. Similar consequence.

I attempted altering the code to provide every subview its personal view mannequin and switched to calling the view mannequin fromonAppear. Nonetheless no pleasure.

I thought of it for a couple of minutes, then switched to working the venture beneath Xcode 14.1 beta 2 and iOS 16.1… and it labored! Largely. Nonetheless just a few glitches in updating the interface, however total it was significantly better.

I then ran the 14.1 model of the code again on the iOS 16.0 simulator and it failed once more, because it additionally did once I ran it straight on my 14 Professional Max.

I then tried one other experiment, going from a primary Record loop like this…

var physique: some View {
Record {
ForEach(customers) { consumer in
NavigationLink(vacation spot: DetailsView(consumer: consumer)) {
MainListCardView(consumer: consumer)
}
}
.navigationTitle("RequestBuilder")
}
}

To utilizing a LazyVStack and emulating a listing.

var physique: some View {
ScrollView {
LazyVStack {
ForEach(customers) { consumer in
NavigationLink(vacation spot: DetailsView(consumer: consumer)) {
HStack {
MainListCardView(consumer: consumer)
.accentColor(Coloration(UIColor.label))
Spacer()
Picture(systemName: "chevron.proper")
.foregroundColor(.secondary)
}
.padding(.vertical, 4)
}
}
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Coloration(.secondarySystemGroupedBackground))
.cornerRadius(16)
.padding(16)
.navigationTitle("RequestBuilder")
}
.background(Coloration(.systemGroupedBackground))
}

Which gave the next consequence.

The identical precise views used from inside a LazyVStack and ScrollView labored. Inside a Record, nevertheless, they didn’t.

What’s appears to be obvious from all of that is that there’s a bug within the iOS 16.0 implementation of SwiftUI the place theonReceive and onAppear handlers for checklist components usually are not all the time being referred to as.

It’s in all probability associated to the truth that, beneath iOS 16, Record views are not backed by desk views however by assortment views. And in altering the implementation Apple’s engineers launched just a few bugs within the course of which appear to have been corrected in iOS 16.1.

Regardless, the tip result’s that in case your utility was depending on that conduct then it should not work appropriately beneath iOS 16 and there’s little to nothing you are able to do about it (in need of re-releasing your app with stated checklist views changed with LazyVStacks).

In a super world, it’s best to simply be capable to go into your Xcode venture settings and hyperlink your utility to a identified model of the SwiftUI library and be insulated from such modifications like we do with CocoaPods or the SPM… however that’s not the way it works.

As I’m certain you’re nicely conscious, every model of SwiftUI is explicitly tied to a selected model of iOS. SwiftUI 1.0 to iOS 13, SwiftUI 2.0 to iOS 14, and so forth.

Positive, Apple will finally launch iOS 16.1 and that can in all probability repair the issue — for iOS 16.1 customers. However any machine not up to date and nonetheless working iOS 16.0 will likely be damaged. Without end.

I’ve written about this repeatedly, and about how Jetpack Compose on Android does issues in a different way. There you’ll be able to hyperlink your utility with the newest model of the library and use virtually all of these options in that library… no matter how outdated that machine is and what model of Android is working.

However on iOS? Nope. Can’t try this. Wish to use some actually cool, extremely superior SwiftUI characteristic like pull-to-refresh?

No drawback. Simply ensure that your complete consumer base is on iOS 15.

It’s… irritating.

And utterly pointless.

Word that the Record change from being backed by aUITableView can even trigger points in case you have been trying to fashion that desk view utilizing UITableView.look modifiers with a view to do another super-advanced options like altering the checklist background shade.

We did that in iOS 13 to work round SwiftUI’s limitations and it labored… till iOS 14 got here out and broke the power to do this. We did get it again once more in iOS 15, so nice!

Solely to lose it once more in iOS 16.

However don’t fear! In any case of those years, SwiftUI 4 has your again. Simply use the background modifier coupled with the brand new scrollContentBackground modifier…

Which is just out there on iOS 16 and higher and, in fact, isn’t back-ported to earlier variations of iOS and SwiftUI.

However I’m certain you’ll be able to determine a approach to make use of the brand new characteristic in addition to sustaining all of that outdated code for backwards compatibility. That’s not messy or inconvenient in any respect.

Proper?

Don’t get me flawed. I really like SwiftUI and I actually suppose it’s the way forward for the iOS platform. And macOS. And elsewhere.

I’d like to apply it to all of my platforms, on all of my tasks. However so long as extremely superior options like tabbing and discipline focus or refreshable are restricted to probably the most present model of the OS… then we now have issues.

Sure. We are able to shim and fall again to UIKit and write our personal navigation stacks and textual content views and use UIViewControllerRepresentables and write UIHostingControllers

However all of that shouldn’t be essential.

Let me repeat that.

All of that shouldn’t be essential.

Hey. Apple!

Android bought it proper.

Possibly we must always take into consideration doing the identical.

That’s it for right now. As all the time depart feedback and questions beneath and hit the like button if you wish to see extra.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments