Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,29 @@
// StripePaymentSheet
//

@_spi(STP) import StripePaymentsUI
import UIKit

extension UIImageView {
// Helper extension for downloading and setting image. Optionally process it before setting.
// On failure or no URL, set fallback image.
// - shimmeringImage: If provided, displayed with a shimmer animation overlay while the download is in progress.
func setImage(
with url: URL?,
processOnDownloadedImage: ((UIImage) -> UIImage)? = nil,
fallbackImage: UIImage
fallbackImage: UIImage,
shimmeringImage: UIImage?
) {
guard let url else {
self.image = fallbackImage
return
}

if let shimmeringImage {
self.image = shimmeringImage
addShimmer()
}

// We use `tag` to ensure that if we call `setImage(with:)` multiple times,
// we ONLY set the image from the `urlString` for the last `urlString` passed.
//
Expand All @@ -30,15 +38,35 @@ extension UIImageView {
await MainActor.run {
if self?.tag == url.hashValue {
self?.image = processedImage
self?.removeShimmer()
}
}
} catch {
await MainActor.run {
if self?.tag == url.hashValue {
self?.image = fallbackImage
self?.removeShimmer()
}
}
}
}
}

func addShimmer() {
removeShimmer()
let shimmer = ShimmerView()
shimmer.translatesAutoresizingMaskIntoConstraints = false
clipsToBounds = true
addSubview(shimmer)
NSLayoutConstraint.activate([
shimmer.topAnchor.constraint(equalTo: topAnchor),
shimmer.bottomAnchor.constraint(equalTo: bottomAnchor),
shimmer.leadingAnchor.constraint(equalTo: leadingAnchor),
shimmer.trailingAnchor.constraint(equalTo: trailingAnchor),
])
}

func removeShimmer() {
subviews.compactMap { $0 as? ShimmerView }.forEach { $0.stopShimmering() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,14 @@ extension SavedPaymentMethodCollectionView {
if let cardArtURL = paymentMethod.cardArtCDNURL(cardArtEnabled: cardArtEnabled) {
if paymentMethodLogo.tag != cardArtURL.hashValue {
paymentMethodLogo.tag = cardArtURL.hashValue
paymentMethodLogo.image = nil
paymentMethodLogo.image = STPImageLibrary.cardBrandChoiceImage()
paymentMethodLogoHeightConstraint.constant = CGFloat(STPPaymentMethod.cardArtHeight)
paymentMethodLogo.addShimmer()
}
Task {
let image = try? await DownloadManager.sharedManager.downloadImage(url: cardArtURL)
guard paymentMethodLogo.tag == cardArtURL.hashValue else { return }
paymentMethodLogo.removeShimmer()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to remove shimmer on line 376?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line 376 is showing me:

                                    paymentMethodLogo.image = paymentMethodCellImage

Removing the shimmer as proposed in the PR (on line 371) seems appropriate since we've added it prior to loading, and we should be removing it irregardless of the card art image loads or we fallback to the network logo.

if let image {
paymentMethodLogo.image = image.roundedWithBorder(radius: 3)
paymentMethodLogoHeightConstraint.constant = CGFloat(STPPaymentMethod.cardArtHeight)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,8 @@ extension RowButton {
if appearance.cardArtEnabled {
imageView.setImage(with: paymentMethod.cardArtCDNURL(cardArtEnabled: appearance.cardArtEnabled),
processOnDownloadedImage: { $0.roundedWithBorder(radius: 3) },
fallbackImage: savedPaymentMethodRowImage)
fallbackImage: savedPaymentMethodRowImage,
shimmeringImage: STPImageLibrary.cardBrandChoiceImage())
} else {
imageView.image = savedPaymentMethodRowImage
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// ShimmerView.swift
// StripePaymentSheet
//

import UIKit

/// A view that displays a sliding shimmer animation overlay.
/// Used as a loading indicator while card art images are downloaded.
class ShimmerView: UIView {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add some snapshot tests for this view?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't consider it due to the animation.

We could write some unit tests that test to make sure the gradient has various alpha components, but that doesn't seem too valuable?

private let gradientLayer = CAGradientLayer()

override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = false
isAccessibilityElement = false
clipsToBounds = true
setupGradient()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}

private func setupGradient() {
gradientLayer.colors = [
UIColor.white.withAlphaComponent(0.0).cgColor,
UIColor.white.withAlphaComponent(0.4).cgColor,
UIColor.white.withAlphaComponent(0.0).cgColor,
]
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
gradientLayer.locations = [-0.5, -0.25, 0.0]
layer.addSublayer(gradientLayer)

let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-0.5, -0.25, 0.0]
animation.toValue = [1.0, 1.25, 1.5]
animation.duration = 1.2
animation.repeatCount = .infinity
gradientLayer.add(animation, forKey: "shimmer")
}

func stopShimmering() {
gradientLayer.removeAllAnimations()
removeFromSuperview()
}
}
Loading