// // AttachmentThumbnailController.swift // ComposeUI // // Created by Shadowfacts on 11/10/21. // Copyright © 2021 Shadowfacts. All rights reserved. // import SwiftUI import Photos import TuskerComponents class AttachmentThumbnailController: ViewController { unowned let parent: ComposeController let attachment: DraftAttachment @Published private var image: UIImage? @Published private var gifController: GIFController? @Published private var fullSize: Bool = false init(attachment: DraftAttachment, parent: ComposeController) { self.attachment = attachment self.parent = parent } func loadImageIfNecessary(fullSize: Bool) { if (gifController != nil) || (image != nil && self.fullSize) { return } self.fullSize = fullSize switch attachment.data { case .editing(_, let kind, let url): switch kind { case .image: Task { @MainActor in self.image = await parent.fetchAttachment(url) } case .video, .gifv: 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) } case .audio, .unknown: break } case .asset(let id): guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil).firstObject else { return } 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 guard let data else { return } if typeIdentifier == UTType.gif.identifier { self.gifController = GIFController(gifData: data) } else { let image = UIImage(data: data) DispatchQueue.main.async { self.image = image } } } } else { let size: CGSize if fullSize { size = PHImageManagerMaximumSize } else { // currently only used as thumbnail in ComposeAttachmentRow size = CGSize(width: 80, height: 80) } PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in DispatchQueue.main.async { self.image = image } } } case .drawing(let drawing): image = drawing.imageInLightMode(from: drawing.bounds) 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.gifController = GIFController(gifData: data) } else 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 gifController = controller.gifController { GIFViewWrapper(controller: gifController) } else 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 } } } private struct GIFViewWrapper: UIViewRepresentable { typealias UIViewType = GIFImageView @State var controller: GIFController 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) { } }