Skip to content

Commit

Permalink
[iOS VMD Flow] Change how we handle image error (#187)
Browse files Browse the repository at this point in the history
* [iOS VMD Flow] Change how we handle image error

* Fix lint
  • Loading branch information
hugolefrancois authored Sep 26, 2023
1 parent f62529b commit 3d449d9
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ fun ImageShowcaseView(imageShowcaseViewModel: ImageShowcaseViewModel) {
contentScale = ContentScale.Crop
)

ComponentShowcaseTitle(viewModel.complexPlaceholderImageTitle)
ComponentShowcaseTitle(viewModel.placeholderNoImageTitle)

val imageModifier = Modifier
.fillMaxWidth()
Expand All @@ -106,7 +106,7 @@ fun ImageShowcaseView(imageShowcaseViewModel: ImageShowcaseViewModel) {

VMDImage(
modifier = imageModifier,
viewModel = viewModel.complexPlaceholderImage,
viewModel = viewModel.placeholderNoImage,
contentScale = ContentScale.Crop,
placeholderContentScale = ContentScale.Crop,
placeholder = { placeholderImageResource, state ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ interface ImageShowcaseViewModel : ShowcaseViewModel {
val placeholderImageTitle: VMDTextViewModel
val placeholderImage: VMDImageViewModel

val complexPlaceholderImageTitle: VMDTextViewModel
val complexPlaceholderImage: VMDImageViewModel
val placeholderNoImageTitle: VMDTextViewModel
val placeholderNoImage: VMDImageViewModel

val placeholderInvalidImageTitle: VMDTextViewModel
val placeholderInvalidImage: VMDImageViewModel

val placeholderLoadingImageTitle: VMDTextViewModel
val placeholderLoadingImage: VMDImageViewModel
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,27 @@ class ImageShowcaseViewModelImpl(i18N: I18N, coroutineScope: CoroutineScope) : S
contentDescription = i18N[KWordTranslation.IMAGE_SHOWCASE_PLACEHOLDER_IMAGE_TITLE]
)

override val complexPlaceholderImageTitle = text(i18N[KWordTranslation.IMAGE_SHOWCASE_COMPLEX_PLACEHOLDER_IMAGE_TITLE])
override val complexPlaceholderImage = remoteImage(
override val placeholderNoImageTitle = text(i18N[KWordTranslation.IMAGE_SHOWCASE_PLACEHOLDER_NO_IMAGE_TITLE])

override val placeholderNoImage = remoteImage(
imageUrl = null,
placeholderImageResource = SampleImageResource.IMAGE_PLACEHOLDER,
contentDescription = i18N[KWordTranslation.IMAGE_SHOWCASE_COMPLEX_PLACEHOLDER_IMAGE_TITLE]
contentDescription = i18N[KWordTranslation.IMAGE_SHOWCASE_PLACEHOLDER_NO_IMAGE_TITLE]
)

override val placeholderInvalidImageTitle = text(i18N[KWordTranslation.IMAGE_SHOWCASE_PLACEHOLDER_INVALID_IMAGE_TITLE])

override val placeholderInvalidImage = remoteImage(
imageUrl = "https://invalidimageimageurl.ca/no_image.jpeg",
placeholderImageResource = SampleImageResource.IMAGE_PLACEHOLDER,
contentDescription = i18N[KWordTranslation.IMAGE_SHOWCASE_PLACEHOLDER_INVALID_IMAGE_TITLE]
)

override val placeholderLoadingImageTitle = text(i18N[KWordTranslation.IMAGE_SHOWCASE_PLACEHOLDER_LOADING_IMAGE_TITLE])

override val placeholderLoadingImage = remoteImage(
imageUrl = "https://www.nasa.gov/sites/default/files/thumbnails/image/main_image_star-forming_region_carina_nircam_final-5mb.jpg",
placeholderImageResource = SampleImageResource.IMAGE_PLACEHOLDER,
contentDescription = i18N[KWordTranslation.IMAGE_SHOWCASE_PLACEHOLDER_LOADING_IMAGE_TITLE]
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ class ImageShowcaseViewModelPreview : VMDViewModelImpl(MainScope()), ImageShowca
override val placeholderImageTitle = text("Placeholder image")
override val placeholderImage = remoteImage(null, SampleImageResource.IMAGE_PLACEHOLDER)

override val complexPlaceholderImageTitle = text("Complex placeholder image")
override val complexPlaceholderImage = remoteImage(null, SampleImageResource.IMAGE_PLACEHOLDER)
override val placeholderNoImageTitle = text("Placeholder no image")
override val placeholderNoImage = remoteImage(null, SampleImageResource.IMAGE_PLACEHOLDER)

override val placeholderInvalidImageTitle = text("Placeholder invalid image")
override val placeholderInvalidImage = remoteImage("https://invalidimageimageurl.ca/no_image.jpeg", SampleImageResource.IMAGE_PLACEHOLDER)

override val placeholderLoadingImageTitle = text("Long loading image")
override val placeholderLoadingImage =
remoteImage(
"https://www.nasa.gov/sites/default/files/thumbnails/image/main_image_star-forming_region_carina_nircam_final-5mb.jpg",
SampleImageResource.IMAGE_PLACEHOLDER
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"home_component_progress": "Progress",
"home_component_list": "List",
"home_component_snackbar": "Snackbar",
"image_showcase_complex_placeholder_image_title": "Complex placeholder",
"image_showcase_placeholder_no_image_title": "No image placeholder",
"image_showcase_placeholder_invalid_image_title": "Invalid image placeholder",
"image_showcase_placeholder_loading_image_title": "Long loading image",
"home_animations_title": "Animations",
"home_animation_easing": "Animation Easing",
"image_showcase_title": "Image showcase",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct ImageShowcaseView: RootViewModelView {
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading, spacing: 40) {
LazyVStack(alignment: .leading, spacing: 40) {
ComponentShowcaseSectionView(viewModel.localImageTitle) {
VMDImage(viewModel.localImage)
.resizable()
Expand Down Expand Up @@ -62,38 +62,42 @@ struct ImageShowcaseView: RootViewModelView {
})
}

ComponentShowcaseSectionView(viewModel.complexPlaceholderImageTitle) {
VMDImage(viewModel.complexPlaceholderImage)
ComponentShowcaseSectionView(viewModel.placeholderNoImageTitle) {
VMDImage(viewModel.placeholderNoImage)
.resizable()
.placeholder({ status, progress, placeholderImage in
Color.gray.opacity(0.25)
.frame(maxWidth: .infinity)
.aspectRatio(imageAspectRatio, contentMode: .fill)
.overlay(
VStack(spacing: 10) {
if let placeholderImage = placeholderImage {
placeholderImage
.resizable()
.aspectRatio(imageAspectRatio, contentMode: .fill)
.frame(width: 50 * imageAspectRatio, height: 50)
}

switch status {
case .empty:
Text("There is no image to display")
.style(.subheadline)
case .loading:
Text("Loading \(progress.fractionCompleted)")
.style(.subheadline)
case .error:
Text("Unable to load the remote image")
.style(.subheadline)
default:
EmptyView()
}
}
)
})
.placeholder { progress, placeholderImage in
ImageShowcasePlaceHolder(
progress: progress,
placeholderImage: placeholderImage,
imageAspectRatio: imageAspectRatio
)
}
}

ComponentShowcaseSectionView(viewModel.placeholderInvalidImageTitle) {
VMDImage(viewModel.placeholderInvalidImage)
.resizable()
.placeholder { progress, placeholderImage in
ImageShowcasePlaceHolder(
progress: progress,
placeholderImage: placeholderImage,
imageAspectRatio: imageAspectRatio
)
}
}

ComponentShowcaseSectionView(viewModel.placeholderLoadingImageTitle) {
VMDImage(viewModel.placeholderLoadingImage)
.resizable()
.placeholder { progress, placeholderImage in
ImageShowcasePlaceHolder(
progress: progress,
placeholderImage: placeholderImage,
imageAspectRatio: imageAspectRatio
)
}
.aspectRatio(imageAspectRatio, contentMode: .fill)
.clipped()
}
}
.frame(maxWidth: .infinity, alignment: .leading)
Expand All @@ -116,3 +120,38 @@ struct ImageShowcaseViewPreviews: PreviewProvider {
}
}
}

struct ImageShowcasePlaceHolder: View {
let progress: Progress
let placeholderImage: Image?
let imageAspectRatio: CGFloat

@Environment(\.vmdImageLoadingStatus) private var loadingStatus: VMDImageLoadingStatus

var body: some View {
Color.gray.opacity(0.25)
.frame(maxWidth: .infinity)
.aspectRatio(imageAspectRatio, contentMode: .fill)
.overlay(
VStack(spacing: 10) {
if let placeholderImage {
placeholderImage
.resizable()
.aspectRatio(imageAspectRatio, contentMode: .fill)
.frame(width: 50 * imageAspectRatio, height: 50)
}

switch loadingStatus {
case .loading:
Text("Loading \(progress.fractionCompleted)")
.style(.subheadline)
case .error:
Text("Unable to load the remote image")
.style(.subheadline)
case .success:
EmptyView()
}
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ public extension VMDImage {
}
}

@available(*, deprecated, message: "Use placeholder(status:progress:placeholderImage) instead")
func placeholder<Content: View>(@ViewBuilder _ content: @escaping (_ progress: Progress, _ placeholderImage: Image?) -> Content) -> VMDImage {
configure {
$0
Expand All @@ -38,20 +37,4 @@ public extension VMDImage {
}
}
}

func placeholder<Content: View>(@ViewBuilder _ content: @escaping (_ status: VMDImageLoadingStatus, _ progress: Progress, _ placeholderImage: Image?) -> Content) -> VMDImage {
configure {
$0
} remote: { kfImage, placehoder in
kfImage.placeholder { progress in
content(loadingStatus, progress, placehoder.image)
}
.onSuccess { result in
loadingStatus = .success
}
.onFailure { error in
loadingStatus = .error
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@ import SwiftUI
import TRIKOT_FRAMEWORK_NAME
import Kingfisher

public enum VMDImageLoadingStatus {
case empty
case loading
case success
case error
}

public struct VMDImage: View {
public typealias LocalImageConfiguration = (Image) -> Image
public typealias RemoteImageConfiguration = (KFImage, VMDImageResource) -> KFImage
Expand All @@ -21,7 +14,7 @@ public struct VMDImage: View {
}

@ObservedObject private var observableViewModel: ObservableViewModelAdapter<VMDImageViewModel>
@State var loadingStatus: VMDImageLoadingStatus = .empty
@State var loadingStatus: VMDImageLoadingStatus = .loading

public init(_ viewModel: VMDImageViewModel) {
self.observableViewModel = viewModel.asObservable()
Expand All @@ -38,16 +31,25 @@ public struct VMDImage: View {
})
.accessibilityLabel(viewModel.contentDescription ?? "")
} else if let remoteImage = viewModel.image as? VMDImageDescriptor.Remote {
remoteImageConfigurations.reduce(KFImage(remoteImage.imageURL), { current, config in
config(current, remoteImage.placeholderImageResource)
})
remoteImageConfigurations.reduce(
KFImage(remoteImage.imageURL)
.onFailure { _ in
loadingStatus = .error
}
.onSuccess { _ in
loadingStatus = .success
}, { current, config in
config(current, remoteImage.placeholderImageResource)
}
)
.accessibilityLabel(viewModel.contentDescription ?? "")
.environment(\.vmdImageLoadingStatus, loadingStatus)
} else {
EmptyView()
}
}

public func configure(local configureLocalBlock: @escaping LocalImageConfiguration, remote configureRemoteBlock: @escaping (KFImage, VMDImageResource) -> KFImage) -> VMDImage {
public func configure(local configureLocalBlock: @escaping LocalImageConfiguration, remote configureRemoteBlock: @escaping RemoteImageConfiguration) -> VMDImage {
if viewModel.image is VMDImageDescriptor.Local {
return configureLocalImage(configureLocalBlock)
} else if viewModel.image is VMDImageDescriptor.Remote {
Expand All @@ -68,3 +70,20 @@ public struct VMDImage: View {
return result
}
}

public enum VMDImageLoadingStatus {
case loading
case success
case error
}

public struct VMDImageLoadingStatusKey: EnvironmentKey {
public static let defaultValue = VMDImageLoadingStatus.loading
}

public extension EnvironmentValues {
var vmdImageLoadingStatus: VMDImageLoadingStatus {
get { self[VMDImageLoadingStatusKey.self] }
set { self[VMDImageLoadingStatusKey.self] = newValue }
}
}

0 comments on commit 3d449d9

Please sign in to comment.