Tusker/Packages/ComposeUI/Sources/ComposeUI/Views/AttachmentThumbnailView.swift

250 lines
8.3 KiB
Swift

//
// AttachmentThumbnailView.swift
// ComposeUI
//
// Created by Shadowfacts on 11/10/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import SwiftUI
import Photos
import TuskerComponents
class AttachmentThumbnailController: ViewController {
let attachment: DraftAttachment
@Published private var image: UIImage?
@Published private var fullSize: Bool = false
init(attachment: DraftAttachment) {
self.attachment = attachment
}
func loadImageIfNecessary(fullSize: Bool) {
guard image == nil || (fullSize && !self.fullSize) else {
return
}
self.fullSize = fullSize
switch attachment.data {
case .asset(let id):
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil).firstObject else {
return
}
let size: CGSize
if fullSize {
size = PHImageManagerMaximumSize
} else {
// currently only used as thumbnail in ComposeAttachmentRow
size = CGSize(width: 80, height: 80)
}
// todo: gifs
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
DispatchQueue.main.async {
self.image = image
}
}
case .drawing(_):
// todo
break
case .file(let url, let type):
// todo: videos
if let data = try? Data(contentsOf: url) {
// todo: gifs
if type.conforms(to: .image),
let image = UIImage(data: data) {
if fullSize {
image.prepareForDisplay { prepared in
DispatchQueue.main.async {
self.image = prepared
}
}
} else {
image.prepareThumbnail(of: CGSize(width: 80, height: 80)) { prepared in
DispatchQueue.main.async {
self.image = prepared
}
}
}
}
}
}
}
var view: some SwiftUI.View {
View()
}
struct View: SwiftUI.View {
@EnvironmentObject private var controller: AttachmentThumbnailController
@Environment(\.attachmentThumbnailConfiguration) private var config
var body: some SwiftUI.View {
content
.onAppear {
controller.loadImageIfNecessary(fullSize: config.fullSize)
}
}
@ViewBuilder
private var content: some SwiftUI.View {
if let image = controller.image {
Image(uiImage: image)
.resizable()
.aspectRatio(config.aspectRatio, contentMode: config.contentMode)
} else {
Image(systemName: "photo")
}
}
}
}
struct AttachmentThumbnailConfiguration {
let aspectRatio: CGFloat?
let contentMode: ContentMode
let fullSize: Bool
init(aspectRatio: CGFloat? = nil, contentMode: ContentMode = .fit, fullSize: Bool = false) {
self.aspectRatio = aspectRatio
self.contentMode = contentMode
self.fullSize = fullSize
}
}
private struct AttachmentThumbnailConfigurationEnvironmentKey: EnvironmentKey {
static let defaultValue = AttachmentThumbnailConfiguration()
}
extension EnvironmentValues {
var attachmentThumbnailConfiguration: AttachmentThumbnailConfiguration {
get { self[AttachmentThumbnailConfigurationEnvironmentKey.self] }
set { self[AttachmentThumbnailConfigurationEnvironmentKey.self] = newValue }
}
}
struct AttachmentThumbnailView: View {
let attachment: DraftAttachment
let fullSize: Bool
@State private var gifData: Data? = nil
@State private var image: UIImage? = nil
@State private var imageContentMode: ContentMode = .fill
@State private var imageBackgroundColor: Color = .black
@Environment(\.colorScheme) private var colorScheme: ColorScheme
var body: some View {
if let gifData {
GIFViewWrapper(gifData: gifData)
} else if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: imageContentMode)
.background(imageBackgroundColor)
} else {
Image(systemName: placeholderImageName)
.onAppear(perform: self.loadImage)
}
}
private var placeholderImageName: String {
switch colorScheme {
case .light:
return "photo"
case .dark:
return "photo.fill"
@unknown default:
return "photo"
}
}
private func loadImage() {
switch attachment.data {
case .asset(let id):
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil).firstObject else {
return
}
let size: CGSize
if fullSize {
size = PHImageManagerMaximumSize
} else {
// currently only used as thumbnail in ComposeAttachmentRow
size = CGSize(width: 80, height: 80)
}
let isGIF = PHAssetResource.assetResources(for: asset).contains(where: { $0.uniformTypeIdentifier == UTType.gif.identifier })
if isGIF {
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: nil) { data, typeIdentifier, orientation, info in
if typeIdentifier == UTType.gif.identifier {
self.gifData = data
} else if let data {
let image = UIImage(data: data)
DispatchQueue.main.async {
self.image = image
}
}
}
} else {
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
DispatchQueue.main.async {
self.image = image
}
}
}
case let .drawing(drawing):
image = drawing.imageInLightMode(from: drawing.bounds)
imageContentMode = .fit
imageBackgroundColor = .white
case .file(let url, let type):
if type.conforms(to: .movie) {
let asset = AVURLAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) {
self.image = UIImage(cgImage: cgImage)
}
} else if let data = try? Data(contentsOf: url) {
if type == .gif {
self.gifData = data
} else if type.conforms(to: .image),
let image = UIImage(data: data) {
if fullSize {
image.prepareForDisplay {
self.image = $0
}
} else {
image.prepareThumbnail(of: CGSize(width: 80, height: 80)) {
self.image = $0
}
}
}
}
}
}
}
private struct GIFViewWrapper: UIViewRepresentable {
typealias UIViewType = GIFImageView
@State private var controller: GIFController
init(gifData: Data) {
self._controller = State(wrappedValue: GIFController(gifData: gifData))
}
func makeUIView(context: Context) -> GIFImageView {
let view = GIFImageView()
controller.attach(to: view)
controller.startAnimating()
view.contentMode = .scaleAspectFit
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
return view
}
func updateUIView(_ uiView: GIFImageView, context: Context) {
}
}