I have an issue in SwiftUI where my image from a downloadURL doesn't publish to the view for visible items. The SwiftUI elements that are off the view do get updated as I scroll. The issue is that initially, the image URL is "" (blank) when the view renders. Thefore, the default image is rendered, as expected. Although, when the downloadURL task completes, the image is not updated. If I use a combination of willSet and/or objectWillChange.send() on the attribute, 1 image gets updated, but only 1.
High level flow:
- Get Series data file from Firebase
- Convert series file data to Series objects within the modelView
- For each Series object, get the image from Firebase asynchronously
Here is the view code
import SwiftUI
import struct Kingfisher.KFImage
import Firebase
struct SeriesView: View {
// Firebase auth
@EnvironmentObject var authState: AuthenticationState
// Listen for view model state changes, i.e. series
@ObservedObject var seriesList = SeriesList()
private func signoutTapped() {
authState.signout()
}
// search bar
@State var searchText = ""
@State var isSearching = false
// firebase
//@State private var imageURL = URL(string: "")
@State private var imageURLString = ""
let storage = Storage.storage()
// view boday
var body: some View {
NavigationView {
ScrollView {
SearchBar(searchText: $searchText, isSearching: $isSearching)
LazyVGrid(columns: [
GridItem(.flexible(minimum: 100, maximum: 200), spacing: 16, alignment: .top),
GridItem(.flexible(minimum: 100, maximum: 200), spacing: 16, alignment: .top),
GridItem(.flexible(minimum: 100, maximum: 200), spacing: 16)
], alignment: .leading, spacing: 16, content: {
// filter for search entry
ForEach(seriesList.seriesArray//) { series in
.filter({"\($0.uniqueId)".contains(searchText)
|| "\($0.recordName)".contains(searchText)
|| "\($0.seriesName)".contains(searchText)
|| "\($0.fromYear)".contains(searchText)
|| "\($0.toYear)".contains(searchText)
|| searchText.isEmpty}), id: \.self) { series in
SeriesInfo(series: series)
}
}).padding(.horizontal, 12)
}
.navigationBarTitle(kMyToyBoxText)
.navigationBarItems(trailing: Button(action: signoutTapped, label: {
Image(systemName: "person.circle")
Text("Logout")
}))
}
}
}
struct SeriesInfo: View {
let series: Series
var body: some View {
NavigationLink(
destination: DetailView(series: series.seriesName)) {
VStack(alignment: .leading, spacing: 4) {
KFImage(URL(string: series.imageFirebaseURLString))
.onSuccess { r in
}
.onFailure { error in
}
.placeholder {
// Placeholder while downloading.
kMyToyBoxLogoImage
.resizable()
.font(.largeTitle)
.opacity(0.3)
.scaledToFit()
.cornerRadius(22)
}
.cancelOnDisappear(true) // cancel if scrolled past
.resizable()
.scaledToFit()
.cornerRadius(22)
Text(series.seriesName)
.font(.system(size: 10, weight: .semibold))
.padding(.top, 4)
// convert to strings to avoid commas
Text("\(String(series.fromYear)) - \(String(series.toYear))")
.font(.system(size: 9, weight: .regular))
Spacer()
}
}
}
}
My Series class (also tried as a struct, but it alter the behavior)
// series class
class Series: Identifiable, ObservableObject {
var id = UUID()
let uniqueId: String
let recordName: String
let seriesName: String
let fromYear: Int
let toYear: Int
let isMyToyBoxActive: Bool
let logoImageName: String
// generated data and needs to be published
@Published var imageFirebaseURLString: String
init (uniqueId: String, recordName: String, seriesName: String, fromYear: Int, toYear: Int, isMyToyBoxActive: Bool, logoImageName: String, imageFirebaseURLString: String = "") {
self.uniqueId = uniqueId
self.recordName = recordName
self.seriesName = seriesName
self.fromYear = fromYear
self.toYear = toYear
self.isMyToyBoxActive = isMyToyBoxActive
self.logoImageName = logoImageName
self.imageFirebaseURLString = imageFirebaseURLString
}
}
extension Series: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self).hashValue)
}
}
extension Series: Equatable {
public static func ==(lhs: Series, rhs: Series) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
}
ModelView code - this code functions as expected to populate the view except the photo data from the downloadURL call
// Series listener and data retriever
class SeriesList: ObservableObject {
@Published var seriesArray = [Series]()
init() {
//... code removed for getting Series file data from Firebase
let series: Series = self.getSeriesFromString(stringSeries: $0)
self.seriesArray.append(series)
// set the image
let storageRefImage = storage.reference().child(kSeriesImageRoot)
let seriesImageRef = storageRefImage.child(series.logoImageName)
// local image
seriesImageRef.downloadURL { url, error in
if let error = error {
}
else
{
series.imageFirebaseURLString = url!.absoluteString // <-- code to update the image URL
}
}
}
}

