-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRetryableImageView.swift
More file actions
94 lines (82 loc) · 3.15 KB
/
RetryableImageView.swift
File metadata and controls
94 lines (82 loc) · 3.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// RetryableImageView.swift
// Examples/UIKit/06-FullReference
//
// Mirrors README:
// - § "What you can build > Rich attachments"
//
// Keep README snippets in sync with this file. See SKILL.md §12.
import UIKit
/// UIImageView that loads from a URL via URLSession + a transient cache,
/// with tap-to-retry on failure and a one-shot auto-retry after 5s.
final class RetryableImageView: UIImageView {
private static let cache: NSCache<NSURL, UIImage> = {
let c = NSCache<NSURL, UIImage>()
c.countLimit = 128
return c
}()
private var currentURL: URL?
private var task: URLSessionDataTask?
private var didAutoRetry = false
private var fallbackImage: UIImage?
private let activity = UIActivityIndicatorView(style: .medium)
convenience init() { self.init(frame: .zero) }
override init(frame: CGRect) {
super.init(frame: frame)
contentMode = .scaleAspectFill
clipsToBounds = true
isUserInteractionEnabled = true
backgroundColor = .secondarySystemBackground
activity.translatesAutoresizingMaskIntoConstraints = false
addSubview(activity)
NSLayoutConstraint.activate([
activity.centerXAnchor.constraint(equalTo: centerXAnchor),
activity.centerYAnchor.constraint(equalTo: centerYAnchor),
])
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapped)))
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func load(url: URL?, fallback: UIImage? = nil) {
task?.cancel()
image = nil
didAutoRetry = false
fallbackImage = fallback
currentURL = url
guard let url else {
image = fallback
return
}
if let cached = Self.cache.object(forKey: url as NSURL) {
image = cached
return
}
activity.startAnimating()
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
task = URLSession.shared.dataTask(with: request) { [weak self] data, _, _ in
guard let self else { return }
DispatchQueue.main.async {
self.activity.stopAnimating()
guard self.currentURL == url else { return }
if let data, let img = UIImage(data: data) {
Self.cache.setObject(img, forKey: url as NSURL)
self.image = img
} else {
self.image = self.fallbackImage
self.scheduleAutoRetry(for: url)
}
}
}
task?.resume()
}
@objc private func tapped() {
guard image == nil || image == fallbackImage, let url = currentURL else { return }
load(url: url, fallback: fallbackImage)
}
private func scheduleAutoRetry(for url: URL) {
guard !didAutoRetry else { return }
didAutoRetry = true
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
guard let self, self.currentURL == url else { return }
self.load(url: url, fallback: self.fallbackImage)
}
}
}