Skip to content

Commit

Permalink
Fix infinite loop in iOS 14 when embedding NetworkImage inside HStack (
Browse files Browse the repository at this point in the history
  • Loading branch information
gonzalezreal authored Mar 12, 2022
1 parent f8b8ed0 commit c97f83e
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 55 deletions.
71 changes: 16 additions & 55 deletions Sources/NetworkImage/SwiftUI/NetworkImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,48 +46,14 @@ import SwiftUI
/// .background(Color.yellow)
///
public struct NetworkImage<Content>: View where Content: View {
private enum ViewState: Equatable {
case empty
case success(URL, Image)
case failure

var image: Image? {
guard case .success(_, let image) = self else {
return nil
}
return image
}

var url: URL? {
guard case .success(let url, _) = self else {
return nil
}
return url
}
}

@Environment(\.networkImageLoader) private var imageLoader
@State private var viewState = ViewState.empty
@ObservedObject private var viewModel: NetworkImageViewModel

private var url: URL?
private var scale: CGFloat
private var transaction: Transaction
private var content: (ViewState) -> Content
private var content: (NetworkImageViewModel.State) -> Content

private var viewStatePublisher: AnyPublisher<ViewState, Never> {
switch url {
case .some(let url) where url == viewState.url:
// Avoid loading the same image again after the layout phase
return Empty().eraseToAnyPublisher()
case .some(let url):
return imageLoader.image(for: url, scale: scale)
.map { .success(url, .init(platformImage: $0)) }
.replaceError(with: .failure)
.receive(on: UIScheduler.shared)
.eraseToAnyPublisher()
case .none:
return Just(.failure).eraseToAnyPublisher()
}
private var context: NetworkImageViewModel.Context {
.init(transaction: self.transaction, imageLoader: self.imageLoader)
}

/// Loads and displays an image from the specified URL using
Expand Down Expand Up @@ -122,8 +88,8 @@ public struct NetworkImage<Content>: View where Content: View {
url: url,
scale: scale,
transaction: transaction,
content: { viewState in
RedactedImage(image: viewState.image, content: content)
content: { state in
RedactedImage(image: state.image, content: content)
}
)
}
Expand All @@ -150,11 +116,11 @@ public struct NetworkImage<Content>: View where Content: View {
url: url,
scale: scale,
transaction: transaction,
content: { viewState in
switch viewState {
content: { state in
switch state {
case .empty, .failure:
placeholder()
case .success(_, let image):
case .success(let image):
content(image)
}
}
Expand Down Expand Up @@ -185,11 +151,11 @@ public struct NetworkImage<Content>: View where Content: View {
url: url,
scale: scale,
transaction: transaction,
content: { viewState in
switch viewState {
content: { state in
switch state {
case .empty:
placeholder()
case .success(_, let image):
case .success(let image):
content(image)
case .failure:
fallback()
Expand All @@ -202,21 +168,16 @@ public struct NetworkImage<Content>: View where Content: View {
url: URL?,
scale: CGFloat,
transaction: Transaction,
@ViewBuilder content: @escaping (ViewState) -> Content
@ViewBuilder content: @escaping (NetworkImageViewModel.State) -> Content
) {
self.url = url
self.scale = scale
self.viewModel = .init(url: url, scale: scale)
self.transaction = transaction
self.content = content
}

public var body: some View {
content(self.viewState)
.onReceive(viewStatePublisher) { viewState in
withTransaction(self.transaction) {
self.viewState = viewState
}
}
self.content(self.viewModel.state)
.onAppear { self.viewModel.onAppear(context: self.context) }
}
}

Expand Down
50 changes: 50 additions & 0 deletions Sources/NetworkImage/SwiftUI/NetworkImageViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Combine
import CombineSchedulers
import SwiftUI

final class NetworkImageViewModel: ObservableObject {
struct Context {
var transaction: Transaction
var imageLoader: NetworkImageLoader
}

enum State: Equatable {
case empty(url: URL, scale: CGFloat)
case success(Image)
case failure

var image: Image? {
guard case .success(let image) = self else {
return nil
}
return image
}
}

@Published private(set) var state: State
private var cancellable: AnyCancellable?

init(url: URL?, scale: CGFloat) {
if let url = url {
self.state = .empty(url: url, scale: scale)
} else {
self.state = .failure
}
}

func onAppear(context: Context) {
guard case .empty(let url, let scale) = self.state else {
return
}

self.cancellable = context.imageLoader.image(for: url, scale: scale)
.map { .success(.init(platformImage: $0)) }
.replaceError(with: .failure)
.receive(on: UIScheduler.shared)
.sink { [weak self] state in
withTransaction(context.transaction) {
self?.state = state
}
}
}
}

0 comments on commit c97f83e

Please sign in to comment.