SwiftUI ObservableObject with #Published array - swift

The UI will update when the array is reassigned to a whole new array, but if I perform a remove on the array the UI will not update.
I was thinking maybe since it was still pointing to the same place in memory the UI wouldn’t be notified of the change. However, I added a didSet property observer to print every time the array was changed and the print statement executed on the remove.
I’m at a loss for how to debug further. Any help would be appreciated!
Code:
Here are the views. There is a movie list view which displays a grid of movie views
struct MovieListView: View {
#ObservedObject var viewModel: MovieListViewModel
init(viewModel: MovieListViewModel) {
self.viewModel = viewModel
}
var body: some View {
ZStack {
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: [
GridItem(.flexible(minimum: 100, maximum: 200), alignment: .top),
GridItem(.flexible(minimum: 100, maximum: 200), alignment: .top)
], spacing: 12, content: {
ForEach(0 ..< viewModel.movies.count, id: \.self) { index in
viewModel.viewForMovie(viewModel.movies[index])
}
})
.id(UUID()) //For ignoring animation
}
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color.blue))
.scaleEffect(1.5, anchor: .center)
.opacity(self.viewModel.loadingMovies ? 1 : 0)
}.onAppear { self.viewModel.loadMovies() }
}
}
struct MovieView: View {
#ObservedObject var viewModel: MovieViewModel
var onDelete: (Movie) -> Void
init(viewModel: MovieViewModel, onDelete: #escaping (Movie) -> Void) {
self.viewModel = viewModel
self.onDelete = onDelete
}
var body: some View {
TmdbMovieView(vm: self.viewModel.tmdbViewModel)
.contentShape(RoundedRectangle(cornerRadius: Constants.MovieViews.movieViewCornerRadius, style: .continuous)) //Prevents sharp edges on context menu
.contextMenu {
Button {
self.onDelete(viewModel.movie)
} label: {
Label("Remove from list", systemImage: Constants.Icons.remove)
}
}
}
}
Here's the movie list view model which specifies what view to use for the movies and exposes the movies from the movie list controller
class MovieListViewModel: ObservableObject {
let name: String
var movies: [Movie] {
return self.movieListController.movies
}
#Published var loadingMovies = false
let movieListController: MovieListController
var viewForMovie: (Movie) -> MovieView
init(name: String, movieListController: MovieListController, viewForMovie: #escaping (Movie) -> MovieView) {
self.name = name
self.movieListController = movieListController
self.viewForMovie = viewForMovie
}
func loadMovies() {
DispatchQueue.main.async {
self.loadingMovies = true
}
self.movieListController.getMovies(complete: onLoadMoviesComplete)
}
private func onLoadMoviesComplete(success: Bool) {
if !success {
//TODO: Handle load fail
}
DispatchQueue.main.async {
self.loadingMovies = false
}
}
func deleteMovie(movie: Movie) {
self.movieListController.delete(movie: movie, complete: onDeleteComplete)
}
private func onDeleteComplete(success: Bool) {
if success {
loadMovies() //TODO: Need this because movies already triggered a change, but the view won't update???
} else {
//TODO: Handle delete fail
}
}
}
Here's my view model factory that creates the view model instance and ties the delete callback on the movie view to the view model
class ViewModelFactory {
let movieListController = MovieListController(id: 1)
func makeMovieListViewModel() -> MovieListViewModel {
let viewModel = MovieListViewModel(
name: "Test",
movieListController: self.movieListController,
viewForMovie: { [unowned self] in
MovieView(viewModel: self.makeMovieViewModel(for: $0), onDelete: {_ in})
}
)
viewModel.viewForMovie = { [unowned self] in
MovieView(viewModel: self.makeMovieViewModel(for: $0), onDelete: viewModel.deleteMovie)
}
return viewModel
}
func makeMovieViewModel(for movie: Movie) -> MovieViewModel {
return MovieViewModel(movie: movie)
}
}
And here is the controller which handles actually hitting my api. The get movies function updates the movies array and the UI updates accordingly. However, the delete function does not cause the UI to update
class MovieListController: ObservableObject {
#Published var movies: [Movie] = []
private var id: Int
init(id: Int) {
self.id = id
}
func getMovies(complete: #escaping (Bool) -> Void) {
guard let moviesUrl = URL(string: "\(Constants.Urls.movieLists)/\(id)\(Constants.Urls.moviesPath)") else {
print("Invalid url...")
complete(false)
return
}
URLSession.shared.dataTask(with: moviesUrl) { data, response, error in
guard let data = data else { return }
do {
var decodedMovies = try JSONDecoder().decode([Movie].self, from: data)
decodedMovies.sort { $0.id < $1.id }
self.movies = decodedMovies
complete(true)
} catch {
print("Failed to decode: \(error)")
complete(false)
}
}.resume()
}
func delete(movie: Movie, complete: #escaping (Bool) -> Void) {
guard let deleteMovieUrl = URL(string: "\(Constants.Urls.movieLists)/\(self.id)\(Constants.Urls.moviesPath)/\(movie.id)") else {
print("Invalid url...")
complete(false)
return
}
var request = URLRequest(url: deleteMovieUrl)
request.httpMethod = "DELETE"
URLSession.shared.dataTask(with: request) { data, response, error in
guard let response = response as? HTTPURLResponse,
error == nil else { // check for fundamental networking error
print("error", error ?? "Unknown error")
complete(false)
return
}
guard (200 ... 299) ~= response.statusCode else { // check for http errors
print("statusCode should be 2xx, but is \(response.statusCode)")
print("response = \(response)")
complete(false)
return
}
if let idx = self.movies.firstIndex(where: { $0.id == movie.id }) {
self.movies.remove(at: idx)
}
complete(true)
}.resume()
}
}

Related

How to return to "View Parent" from ASWebAutheticationSession

How to return from the ASWebAutheticationSession completion handler back to the View?
Edit: Just for clearance this is not the original code in my project this is extremely shortened and is just for showcasing what I mean.
Here's an example of a code
struct SignInView: View {
#EnvironmentObject var signedIn: UIState
var body: some View {
let AuthenticationSession = AuthSession()
AuthenticationSession.webAuthSession.presentationContextProvider = AuthenticationSession
AuthenticationSession.webAuthSession.prefersEphemeralWebBrowserSession = true
AuthenticationSession.webAuthSession.start()
}
}
class AuthSession: NSObject, ObservableObject, ASWebAuthenticationPresentationContextProviding {
var webAuthSession = ASWebAuthenticationSession.init(
url: AuthHandler.shared.signInURL()!,
callbackURLScheme: "",
completionHandler: { (callbackURL: URL?, error: Error?) in
// check if any errors appeared
// get code from authentication
// Return to view to move on with code? (return code)
})
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return ASPresentationAnchor()
}
}
So what I'm trying to do is call the sign In process and then get back to the view with the code from the authentication to move on with it.
Could somebody tell me how this may be possible?
Thanks.
Not sure if I'm correctly understanding your question but it is normally done with publishers, commonly with the #Published wrapper, an example:
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
Button {
self.viewModel.signIn(user: "example", password: "example")
}
label: {
Text("Sign in")
}
if self.viewModel.signedIn {
Text("Successfully logged in")
}
else if let error = self.viewModel.signingError {
Text("Error while logging in: \(error.localizedDescription)")
}
}
.padding()
}
}
class ViewModel: ObservableObject {
#Published var signingStatus = SigningStatus.idle
var signedIn: Bool {
if case .success = self.signingStatus { return true }
return false
}
var signingError: Error? {
if case .failure(let error) = self.signingStatus { return error }
return nil
}
func signIn(user: String, password: String) {
self.dummyAsyncProcessWithCompletionHandler { [weak self] success in
guard let self = self else { return }
guard success else {
self.updateSigning(.failure(CustomError(errorDescription: "Login failed")))
return
}
self.updateSigning(.success)
}
}
private func updateSigning(_ status: SigningStatus) {
DispatchQueue.main.async {
self.signingStatus = status
}
}
private func dummyAsyncProcessWithCompletionHandler(completion: #escaping (Bool) -> ()) {
Task {
print("Signing in")
try await Task.sleep(nanoseconds: 500_000_000)
guard Int.random(in: 0..<9) % 2 == 0 else {
print("Error")
completion(false)
return
}
print("Success")
completion(true)
}
}
enum SigningStatus {
case idle
case success
case failure(Error)
}
struct CustomError: Error, LocalizedError {
var errorDescription: String?
}
}

Refresh remotely loaded image in SwiftUI

It's a lot of code and looks daunting, but it's pretty simple--I'm trying to load remote image, and when the image is clicked, I'd like to switch to the next image:
struct TestView: View {
#State var selectedIndex: Int = 0
#State var arrayOfImages: [String] = ["https://s3-media3.fl.yelpcdn.com/bphoto/_0bkRz0wln3URHevWORCkA/o.jpg", "https://s3-media2.fl.yelpcdn.com/bphoto/MDZXc4pDt5xUfXF0Rw6rMw/o.jpg", "https://s3-media3.fl.yelpcdn.com/bphoto/feYg35an2MilNK3dCwwqTQ/o.jpg"]
var body: some View {
RemoteImage(url: arrayOfImages[selectedIndex])
.scaledToFill()
.frame(width: 200, height: 200)
.clipped()
.onTapGesture {
selectedIndex += 1
}
}
}
struct RemoteImage: View {
private enum LoadState {
case loading, success, failure
}
private class Loader: ObservableObject {
var data = Data()
var state = LoadState.loading
init(url: String) {
guard let parsedURL = URL(string: url) else {
fatalError("Invalid URL: \(url)")
}
URLSession.shared.dataTask(with: parsedURL) { data, response, error in
if let data = data, data.count > 0 {
self.data = data
self.state = .success
} else {
self.state = .failure
}
DispatchQueue.main.async {
self.objectWillChange.send()
}
}.resume()
}
}
#StateObject private var loader: Loader
var loading: Image
var failure: Image
var body: some View {
selectImage()
.resizable()
}
init(url: String, loading: Image = Image(""), failure: Image = Image(systemName: "multiply.circle")) {
_loader = StateObject(wrappedValue: Loader(url: url))
self.loading = loading
self.failure = failure
}
private func selectImage() -> Image {
switch loader.state {
case .loading:
return loading
case .failure:
return failure
default:
if let image = UIImage(data: loader.data) {
return Image(uiImage: image)
} else {
return failure
}
}
}
}
Here's the problem: the image doesn't go to the next one when you tap on it. I think it's because the RemoteImage view isn't being reloaded, but I'm not sure how to fix. Any help is appreciated!
I think you are trying to do too much inside RemoteImage, in particular
declaring private #StateObject private var loader: Loader and all that derives from this.
Try this approach with #StateObject var loader = Loader() outside your RemoteImage.
Works for me.
struct ContentView: View {
var body: some View {
TestView()
}
}
struct TestView: View {
#StateObject var loader = Loader() // <-- here
#State var selectedIndex: Int = 0
#State var arrayOfImages: [String] = ["https://s3-media3.fl.yelpcdn.com/bphoto/_0bkRz0wln3URHevWORCkA/o.jpg", "https://s3-media2.fl.yelpcdn.com/bphoto/MDZXc4pDt5xUfXF0Rw6rMw/o.jpg", "https://s3-media3.fl.yelpcdn.com/bphoto/feYg35an2MilNK3dCwwqTQ/o.jpg"]
var body: some View {
RemoteImage(loader: loader) // <--- here
.scaledToFill()
.frame(width: 200, height: 200)
.clipped()
.onTapGesture {
selectedIndex += 1
if selectedIndex < arrayOfImages.count {
loader.load(url: arrayOfImages[selectedIndex]) // <-- here
} else {
//...
}
}
.onAppear {
loader.load(url: arrayOfImages[selectedIndex]) // <--- here
}
}
}
class Loader: ObservableObject {
var data = Data()
var state = LoadState.loading
func load(url: String) { // <--- here
guard let parsedURL = URL(string: url) else {
fatalError("Invalid URL: \(url)")
}
URLSession.shared.dataTask(with: parsedURL) { data, response, error in
if let data = data, data.count > 0 {
self.data = data
self.state = .success
} else {
self.state = .failure
}
DispatchQueue.main.async {
self.objectWillChange.send()
}
}.resume()
}
}
enum LoadState {
case loading, success, failure
}
struct RemoteImage: View {
#ObservedObject var loader: Loader // <--- here
var loading: Image = Image("")
var failure: Image = Image(systemName: "multiply.circle")
var body: some View {
selectImage().resizable()
}
private func selectImage() -> Image {
switch loader.state {
case .loading:
return loading
case .failure:
return failure
default:
if let image = UIImage(data: loader.data) {
return Image(uiImage: image)
} else {
return failure
}
}
}
}

Update View Only After Aync Is Resolved with Completion Handler

I'm trying to update my view, only after the Async call is resolved. In the below code the arrayOfTodos.items comes in asynchronously a little after TodoListApp is rendered. The problem I'm having is that when onAppear runs, self.asyncTodoList.items is always empty since it hasn't received the values of the array yet from the network call. I'm stuck trying to figure out how to hold off on running onAppear until after the Promise is resolved, like with a completion handler?? And depending on the results of the network call, then modify the view. Thanks for any help! I've been stuck on this longer than I'll ever admit!
struct ContentView: View {
#StateObject var arrayOfTodos = AsyncGetTodosNetworkCall()
var body: some View {
TodoListApp(asyncTodoList: arrayOfTodos)
}
}
struct TodoListApp: View {
#ObservedObject var asyncTodoList: AsyncGetTodosNetworkCall
#State private var showPopUp: Bool = false
var body: some View {
NavigationView {
ZStack {
VStack {
Text("Top Area")
Text("List Area")
}
if self.showPopUp == true {
VStack {
Text("THIS IS MY POPUP!")
Text("No Items Added Yet")
}.frame(width: 300, height: 400)
}
}.onAppear {
let arrayItems = self.asyncTodoList
if arrayItems.items.isEmpty {
self.showPopUp = true
}
/*HERE! arrayItems.items.isEmpty is ALWAYS empty when onAppear
runs since it's asynchronous. What I'm trying to do is only
show the popup if the array is empty after the promise is
resolved.
What is happening is even if array resolved with multiple todos,
the popup is still showing because it was initially empty on
first run. */
}
}
}
}
class AsyncGetTodosNetworkCall: ObservableObject {
#AppStorage(DBUser.userID) var currentUserId: String?
private var REF_USERS = DB_BASE.collection(DBCOLLECTION.appUsers)
#Published var items = [TodoItem]()
func fetchTodos(toDetach: Bool) {
guard let userID = currentUserId else {
return
}
let userDoc = REF_USERS.document(String(userID))
.collection(DBCOLLECTION.todos)
.addSnapshotListener({ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No Documents Found")
return
}
self.items = documents.map { document -> TodoItem in
let todoID = document.documentID
let todoName = document.get(ToDo.todoName) as? String ?? ""
let todoCompleted = document.get(Todo.todoCompleted) as? Bool ?? false
return TodoItem(
id: todoID,
todoName: todoName,
todoCompleted: todoCompleted
)
}
})
if toDetach == true {
userDoc.remove()
}
}
}
While preparing my question, i found my own answer. Here it is in case someone down the road might run into the same issue.
struct ContentView: View {
#StateObject var arrayOfTodos = AsyncGetTodosNetworkCall()
#State var hasNoTodos: Bool = false
func getData() {
self.arrayOfTodos.fetchTodos(toDetach: false) { noTodos in
if noTodos {
self.hasNoTodos = true
}
}
}
func removeListeners() {
self.arrayOfTodos.fetchTodos(toDetach: true)
}
var body: some View {
TabView {
TodoListApp(asyncTodoList: arrayOfTodos, hasNoTodos : self.$hasNoTodos)
}.onAppear(perform: {
self.getData()
}).onDisappear(perform: {
self.removeListeners()
})
}
}
struct TodoListApp: View {
#ObservedObject var asyncTodoList: AsyncGetTodosNetworkCall
#Binding var hasNoTodos: Bool
#State private var hidePopUp: Bool = false
var body: some View {
NavigationView {
ZStack {
VStack {
Text("Top Area")
ScrollView {
LazyVStack {
ForEach(asyncTodoList.items) { item in
HStack {
Text("\(item.name)")
Spacer()
Text("Value")
}
}
}
}
}
if self.hasNoTodos == true {
if self.hidePopUp == false {
VStack {
Text("THIS IS MY POPUP!")
Text("No Items Added Yet")
}.frame(width: 300, height: 400)
}
}
}
}
}
}
class AsyncGetTodosNetworkCall: ObservableObject {
#AppStorage(DBUser.userID) var currentUserId: String?
private var REF_USERS = DB_BASE.collection(DBCOLLECTION.appUsers)
#Published var items = [TodoItem]()
func fetchTodos(toDetach: Bool, handler: #escaping (_ noTodos: Bool) -> ()) {
guard let userID = currentUserId else {
handler(true)
return
}
let userDoc = REF_USERS.document(String(userID))
.collection(DBCOLLECTION.todos)
.addSnapshotListener({ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No Documents Found")
handler(true)
return
}
self.items = documents.map { document -> TodoItem in
let todoID = document.documentID
let todoName = document.get(ToDo.todoName) as? String ?? ""
let todoCompleted = document.get(Todo.todoCompleted) as? Bool ?? false
return TodoItem(
id: todoID,
todoName: todoName,
todoCompleted: todoCompleted
)
}
handler(false)
})
if toDetach == true {
userDoc.remove()
}
}
}

How to load next Image while user is looking at first image in the TabView pages carousel with AsyncImageView In SwiftUI?

I have an images carousel that fetching heavy images from a few URLs and displaying asynchronously when user is going to the next page. And the problem is that user need to see a waiting wheel and wait to see the next image.
So the ideal case would be to eliminate the waiting wheel by loading the next image while user still seeing the fist image and I don't know how to do it properly.
If you know the solution please let me know, I will mean a lot to me.
Here's the code:
struct ContentView: View {
#State private var selectedImageToSee: Int = 0
private let imagesUrl: [String] = [
"https://images.unsplash.com/photo-1633125195723-04860dde4545?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzNDIyMDUyMQ&ixlib=rb-1.2.1&q=80&w=3024",
"https://images.unsplash.com/photo-1633090332452-532d6b39422a?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzNDIyMDI1Ng&ixlib=rb-1.2.1&q=80&w=3024",
"https://images.unsplash.com/photo-1591667954789-c25d5affec59?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLGNhcnN8fHx8fHwxNjM0MjIwNzU1&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=3024",
"https://images.unsplash.com/photo-1580316576539-aee1337e80e8?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLGNhcnN8fHx8fHwxNjM0MjIwNzk5&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=3024",
"https://images.unsplash.com/photo-1484976063837-eab657a7aca7?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLGNhcnN8fHx8fHwxNjM0MjIwNjky&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=3024"
]
var body: some View {
ImagesCaruselView(selectedImageToSee: $selectedImageToSee, urls: imagesUrl)
}
}
struct ImagesCaruselView: View {
#Binding var selectedImageToSee: Int
let urls: [String]
var body: some View {
TabView(selection: $selectedImageToSee) {
ForEach(0..<urls.count, id: \.self) { url in
AsyncImageView(
url: URL(string: urls[url])!,
placeholder: { ProgressView().scaleEffect(1.5).progressViewStyle(CircularProgressViewStyle(tint: .gray)) },
image: { Image(uiImage: $0) }
)
.padding()
}
}
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
struct AsyncImageView<Placeholder: View>: View {
#StateObject private var loader: ImageLoader
private let placeholder: Placeholder
private let image: (UIImage) -> Image
init(
url: URL,
#ViewBuilder placeholder: () -> Placeholder,
#ViewBuilder image: #escaping (UIImage) -> Image = Image.init(uiImage:)
) {
self.placeholder = placeholder()
self.image = image
_loader = StateObject(wrappedValue: ImageLoader(url: url, cache: Environment(\.imageCache).wrappedValue))
}
var body: some View {
content
.onAppear(perform: loader.load)
}
private var content: some View {
Group {
if loader.image != nil {
image(loader.image!)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
placeholder
}
}
}
}
protocol ImageCache {
subscript(_ url: URL) -> UIImage? { get set }
}
struct TemporaryImageCache: ImageCache {
private let cache: NSCache<NSURL, UIImage> = {
let cache = NSCache<NSURL, UIImage>()
cache.countLimit = 100 // 100 items
cache.totalCostLimit = 1024 * 1024 * 200 // 200 MB
return cache
}()
subscript(_ key: URL) -> UIImage? {
get { cache.object(forKey: key as NSURL) }
set { newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) }
}
}
import Combine
class ImageLoader: ObservableObject {
#Published var image: UIImage?
private(set) var isLoading = false
private let url: URL
private var cache: ImageCache?
private var cancellable: AnyCancellable?
private static let imageProcessingQueue = DispatchQueue(label: "image-processing")
init(url: URL, cache: ImageCache? = nil) {
self.url = url
self.cache = cache
}
deinit {
cancel()
}
func load() {
guard !isLoading else { return }
if let image = cache?[url] {
self.image = image
return
}
#warning("Caching the image are disabled")
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.handleEvents(receiveSubscription: { [weak self] _ in self?.onStart() },
// receiveOutput: { [weak self] in self?.cache($0) },
receiveCompletion: { [weak self] _ in self?.onFinish() },
receiveCancel: { [weak self] in self?.onFinish() })
.subscribe(on: Self.imageProcessingQueue)
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
func cancel() {
cancellable?.cancel()
}
private func onStart() {
isLoading = true
}
private func onFinish() {
isLoading = false
}
private func cache(_ image: UIImage?) {
image.map { cache?[url] = $0 }
}
}
import SwiftUI
struct ImageCacheKey: EnvironmentKey {
static let defaultValue: ImageCache = TemporaryImageCache()
}
extension EnvironmentValues {
var imageCache: ImageCache {
get { self[ImageCacheKey.self] }
set { self[ImageCacheKey.self] = newValue }
}
}

Update view with observed objects of observed array in swiftui

I'm having an Image holder that would load the thumdnail on init and allow for download later on. My issue is that the view is not updated with the images after I load them. After pressing the load button for the second time, my first images are then displayed.
I'm having trouble finding the reason behind this behaviour.
The image holder :
class MyImage: ObservableObject {
private static let sessionProcessingQueue = DispatchQueue(label: "SessionProcessingQueue")
#Published var thumbnail: UIImage?
#Published var loaded: Bool
var fullName: String {
"\(folderName)/\(fileName)"
}
var onThumbnailSet: ((UIImage?) -> Void)
private var folderName: String
private var fileName: String
private var cancelableThumbnail: AnyCancellable?
private var thumbnailUrl: URL? {
return URL(string: "\(BASE_URL)/thumbnail/\(fullName)")
}
private var downloadUrl: URL? {
return URL(string: "\(BASE_URL)/download/\(fullName)")
}
init(folderName: String, fileName: String) {
self.folderName = folderName
self.fileName = fileName
self.loaded = false
self.loadThumbnail()
}
private func loadThumbnail() {
guard let requestUrl = thumbnailUrl else { fatalError() }
self.cancelableThumbnail = URLSession.shared.dataTaskPublisher(for: requestUrl)
.subscribe(on: Self.sessionProcessingQueue)
.map { UIImage(data: $0.data) }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { (suscriberCompletion) in
switch suscriberCompletion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
}
}, receiveValue: { [weak self] (value) in
self?.objectWillChange.send()
self?.loaded.toggle()
self?.thumbnail = value
})
}
The view :
struct MyView: View {
#ObservedObject var imagesHolder: ImagesHolder = ImagesHolder()
var body: some View {
VStack {
Button(action: {
self.loadImages()
}, label: {
Text("Load images")
})
ForEach(imagesHolder.images, id: \.self) { image in
if image.loaded {
Image(uiImage: image.thumbnail!)
.frame(width: 600, height: 600)
} else {
Text("Not loaded")
}
}
}
}
private func loadImages() -> Void {
loadMediaList(
onLoadDone: { myImages in
myImages.forEach { image in
imagesHolder.append(image)
}
}
)
}
}
The observed object containing the array of loaded images :
class ImagesHolder: ObservableObject {
#Published var images: [MyImage] = []
func append(_ myImage: MyImage) {
objectWillChange.send()
images.append(myImage)
}
}
And finally my data loader :
func loadMediaList(onLoadDone: #escaping (([MyImage]) -> Void)) -> AnyCancellable {
let url = URL(string: "\(BASE_URL)/medias")
guard let requestUrl = url else { fatalError() }
return URLSession.shared.dataTaskPublisher(for: requestUrl)
.subscribe(on: Self.sessionProcessingQueue)
.map { parseJSON(data: $0.data) }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { (suscriberCompletion) in
switch suscriberCompletion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
}
}, receiveValue: { images in
onLoadDone(images);
})
}
What I ended up doing and worked great for me was having a seperate view for the display of my Image like this :
struct MyImageView: View {
#ObservedObject var image: MyImage
init(image: MyImage) {
self.image = image
}
var body: some View {
if image.loaded {
Image(uiImage: image.thumbnail!)
.resizable()
} else {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.frame(width: 100, height: 100, alignment: .center)
}
}
}
struct MyView: View {
#ObservedObject var imagesHolder: ImagesHolder = ImagesHolder()
var body: some View {
VStack {
Button(action: {
self.loadImages()
}, label: {
Text("Load images")
})
ForEach(imagesHolder.images, id: \.self) { image in
MyImageView(image: image)
}
}
}
private func loadImages() -> Void {
loadMediaList(
onLoadDone: { myImages in
myImages.forEach { image in
imagesHolder.append(image)
}
}
)
}
}