// // 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) { } }