From 02daf88db3221b7a40248d94ea16497fde9e8916 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 3 May 2023 23:18:52 -0400 Subject: [PATCH] Support gifs in new thumbnail controller and focused attachment view --- .../Controllers/AttachmentRowController.swift | 2 +- .../AttachmentThumbnailController.swift} | 172 +++++------------- .../Controllers/DraftsController.swift | 2 +- .../ComposeUI/Views/ZoomableScrollView.swift | 4 +- 4 files changed, 49 insertions(+), 131 deletions(-) rename Packages/ComposeUI/Sources/ComposeUI/{Views/AttachmentThumbnailView.swift => Controllers/AttachmentThumbnailController.swift} (60%) diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentRowController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentRowController.swift index 83528e5a..bed01edf 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentRowController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentRowController.swift @@ -155,7 +155,7 @@ class AttachmentRowController: ViewController { Label("Delete", systemImage: "trash") } } previewIfAvailable: { - AttachmentThumbnailView(attachment: attachment, fullSize: true) + ControllerView(controller: { controller.thumbnailController }) } switch controller.descriptionMode { diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/AttachmentThumbnailView.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentThumbnailController.swift similarity index 60% rename from Packages/ComposeUI/Sources/ComposeUI/Views/AttachmentThumbnailView.swift rename to Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentThumbnailController.swift index 9c7d5f27..a476bb07 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/AttachmentThumbnailView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentThumbnailController.swift @@ -1,5 +1,5 @@ // -// AttachmentThumbnailView.swift +// AttachmentThumbnailController.swift // ComposeUI // // Created by Shadowfacts on 11/10/21. @@ -14,14 +14,16 @@ class AttachmentThumbnailController: ViewController { let attachment: DraftAttachment @Published private var image: UIImage? + @Published private var gifController: GIFController? @Published private var fullSize: Bool = false + @Published private var imageBackground: Color? init(attachment: DraftAttachment) { self.attachment = attachment } func loadImageIfNecessary(fullSize: Bool) { - guard image == nil || (fullSize && !self.fullSize) else { + if (gifController != nil) || (image != nil && fullSize == self.fullSize) { return } self.fullSize = fullSize @@ -31,29 +33,48 @@ class AttachmentThumbnailController: ViewController { guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil).firstObject else { return } - let size: CGSize - if fullSize { - size = PHImageManagerMaximumSize + 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 { - // 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 + 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(_): - // todo - break - + case .drawing(let drawing): + image = drawing.imageInLightMode(from: drawing.bounds) + case .file(let url, let type): - // todo: videos - if let data = try? Data(contentsOf: url) { - // todo: gifs - if type.conforms(to: .image), + 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 @@ -90,7 +111,9 @@ class AttachmentThumbnailController: ViewController { @ViewBuilder private var content: some SwiftUI.View { - if let image = controller.image { + if let gifController = controller.gifController { + GIFViewWrapper(controller: gifController) + } else if let image = controller.image { Image(uiImage: image) .resizable() .aspectRatio(config.aspectRatio, contentMode: config.contentMode) @@ -124,115 +147,10 @@ extension EnvironmentValues { } } -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)) - } + @State var controller: GIFController func makeUIView(context: Context) -> GIFImageView { let view = GIFImageView() diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/DraftsController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/DraftsController.swift index f54a5d61..62651c44 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/DraftsController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/DraftsController.swift @@ -134,7 +134,7 @@ private struct DraftRow: View { HStack(spacing: 8) { ForEach(draft.draftAttachments) { attachment in - AttachmentThumbnailView(attachment: attachment, fullSize: false) + ControllerView(controller: { AttachmentThumbnailController(attachment: attachment) }) .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 5)) .frame(height: 50) diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/ZoomableScrollView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/ZoomableScrollView.swift index 54b4f7eb..80d46732 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/ZoomableScrollView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/ZoomableScrollView.swift @@ -74,7 +74,8 @@ struct ZoomableScrollView: UIViewControllerRepresentable { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - if host.view.intrinsicContentSize != lastIntrinsicSize { + if !host.view.intrinsicContentSize.equalTo(.zero), + host.view.intrinsicContentSize != lastIntrinsicSize { self.lastIntrinsicSize = host.view.intrinsicContentSize let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom @@ -83,7 +84,6 @@ struct ZoomableScrollView: UIViewControllerRepresentable { let widthScale = maxWidth / host.view.intrinsicContentSize.width let minScale = min(widthScale, heightScale) let maxScale = minScale >= 1 ? minScale + 2 : 2 - print("min: \(minScale), max: \(maxScale)") scrollView.minimumZoomScale = minScale scrollView.maximumZoomScale = maxScale scrollView.zoomScale = minScale