-
Notifications
You must be signed in to change notification settings - Fork 2
/
PhotoThumbnailView.swift
137 lines (112 loc) · 3.95 KB
/
PhotoThumbnailView.swift
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//
// PhotoThumbnailView.swift
// PhotoCompress
//
// Created by Cirno MainasuK on 2021-7-9.
// Copyright © 2021 MainasuK. All rights reserved.
//
import os.log
import SwiftUI
import Combine
import Photos
import Kingfisher
@MainActor
class PhotoThumbnailViewModel: ObservableObject {
let logger = Logger(subsystem: "PhotoThumbnailViewModel", category: "ViewModel")
let context: AppContext
private var imageManager: PHImageManager {
context.photoService.imageManager
}
var disposeBag = Set<AnyCancellable>()
// input
let index: Int
let isAppear = CurrentValueSubject<Bool, Never>(false)
let frame = CurrentValueSubject<CGRect, Never>(.zero)
var imageRequestID: PHImageRequestID?
// output
@Published var photo: UIImage?
init(context: AppContext, index: Int) {
self.context = context
self.index = index
Publishers.CombineLatest(
isAppear.removeDuplicates(),
frame.map { $0.size }.removeDuplicates()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] isAppear, size in
guard let self = self else { return }
guard isAppear, size != .zero else {
self.invalid()
return
}
self.fetchThumbnail(targetSize: size)
}
.store(in: &disposeBag)
}
func fetchThumbnail(targetSize: CGSize) {
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch assert: \(self.index) size: \(targetSize.debugDescription)")
let options = PHImageRequestOptions()
options.deliveryMode = .opportunistic
options.isNetworkAccessAllowed = true
cancelFetch()
let asset = context.photoService.photos[index]
self.imageRequestID = self.imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFit, options: options) { [weak self] image, options in
guard let self = self else { return }
if let image = image {
assert(Thread.isMainThread)
self.photo = image
}
}
}
private func cancelFetch() {
if let imageRequestID = self.imageRequestID {
imageManager.cancelImageRequest(imageRequestID)
}
imageRequestID = nil
}
func invalid() {
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): invalid \(self.index)")
cancelFetch()
photo = nil
}
}
struct PhotoThumbnailView: View {
@ObservedObject var viewModel: PhotoThumbnailViewModel
var body: some View {
GeometryReader { proxy in
VStack {
if let photo = viewModel.photo {
Image(uiImage: photo)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Color(uiColor: .systemFill)
}
}
.onAppear {
viewModel.isAppear.value = true
}
.onDisappear {
viewModel.isAppear.value = false
}
.preference(
key: PhotoThumbnailFramePreferenceKey.self,
value: proxy.frame(in: .global)
)
.onPreferenceChange(PhotoThumbnailFramePreferenceKey.self) { frame in
viewModel.frame.value = frame
}
}
.clipped()
}
}
struct PhotoThumbnailView_Previews: PreviewProvider {
static var previews: some View {
PhotoThumbnailView(viewModel: PhotoThumbnailViewModel(context: AppContext.shared, index: 0))
.frame(width: 300, height: 300)
}
}
struct PhotoThumbnailFramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) { }
}