Change loading images in compose screen

This commit is contained in:
Marcin Czachursk 2023-04-08 13:20:06 +02:00
parent 90222ca16b
commit 02871e07a0
10 changed files with 152 additions and 49 deletions

View File

@ -0,0 +1,63 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import AVFoundation
import UIKit
public class ImageCompressService {
public static let shared = ImageCompressService()
private init() { }
public func compressImageFrom(url: URL) async -> Data? {
return await withCheckedContinuation { continuation in
let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else {
continuation.resume(returning: nil)
return
}
let maxPixelSize: Int
if Bundle.main.bundlePath.hasSuffix(".appex") {
maxPixelSize = 1536
} else {
maxPixelSize = 4096
}
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize
] as [CFString: Any] as CFDictionary
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else {
continuation.resume(returning: nil)
return
}
let data = NSMutableData()
guard let imageDestination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil) else {
continuation.resume(returning: nil)
return
}
let isPNG: Bool = {
guard let utType = cgImage.utType else { return false }
return (utType as String) == UTType.png.identifier
}()
let destinationProperties = [
kCGImageDestinationLossyCompressionQuality: isPNG ? 1.0 : 0.85
] as CFDictionary
CGImageDestinationAddImage(imageDestination, cgImage, destinationProperties)
CGImageDestinationFinalize(imageDestination)
continuation.resume(returning: data as Data)
}
}
}

View File

@ -170,7 +170,6 @@
F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B0885F29943498002AB40A /* OtherSectionView.swift */; };
F8B08862299435C9002AB40A /* SupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B08861299435C9002AB40A /* SupportView.swift */; };
F8CAE64029B8E6E1001E0372 /* UIApplication+Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CAE63F29B8E6E1001E0372 /* UIApplication+Window.swift */; };
F8CEEDFA29ABAFD200DBED66 /* ImageFileTranseferable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CEEDF929ABAFD200DBED66 /* ImageFileTranseferable.swift */; };
F8D5444329D4066C002225D6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D5444229D4066C002225D6 /* AppDelegate.swift */; };
F8DF38E429DD68820047F1AA /* ViewOffsetKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */; };
F8DF38E629DDB98A0047F1AA /* SocialsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */; };
@ -349,7 +348,6 @@
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-001.xcdatamodel"; sourceTree = "<group>"; };
F8CAE63F29B8E6E1001E0372 /* UIApplication+Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Window.swift"; sourceTree = "<group>"; };
F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-005.xcdatamodel"; sourceTree = "<group>"; };
F8CEEDF929ABAFD200DBED66 /* ImageFileTranseferable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFileTranseferable.swift; sourceTree = "<group>"; };
F8D5444229D4066C002225D6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewOffsetKey.swift; sourceTree = "<group>"; };
F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialsSectionView.swift; sourceTree = "<group>"; };
@ -491,7 +489,6 @@
F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */,
F8764186298ABB520057D362 /* ViewState.swift */,
F89AC00429A1F9B500F4159F /* AppMetadata.swift */,
F8CEEDF929ABAFD200DBED66 /* ImageFileTranseferable.swift */,
F88AB05229B3613900345EDE /* PhotoUrl.swift */,
F85D4DFD29B78C8400345267 /* HashtagModel.swift */,
F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */,
@ -1122,7 +1119,6 @@
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */,
F8121CA8298A86D600B466C7 /* InstanceRowView.swift in Sources */,
F873F14C29BDB67300DE72D1 /* UIImage+Rounded.swift in Sources */,
F8CEEDFA29ABAFD200DBED66 /* ImageFileTranseferable.swift in Sources */,
F8D5444329D4066C002225D6 /* AppDelegate.swift in Sources */,
F802884F297AEED5000BDD51 /* DatabaseError.swift in Sources */,
F86A4307299AA5E900DF7645 /* ThanksView.swift in Sources */,

View File

@ -517,9 +517,7 @@ struct ComposeView: View {
// Now we have to get from photos images as JPEG.
for item in self.photosAttachment.filter({ $0.photoData == nil }) {
if let data = try await item.loadData() {
item.photoData = data
}
try await item.loadImage()
}
// Open again the keyboard.
@ -550,16 +548,20 @@ struct ComposeView: View {
private func upload(_ photoAttachment: PhotoAttachment) async {
do {
// We have to have binary data and image shouldn't be uploaded yet.
guard let photoData = photoAttachment.photoData, photoAttachment.uploadedAttachment == nil else {
// Image shouldn't be uploaded yet.
guard photoAttachment.uploadedAttachment == nil else {
return
}
guard let image = UIImage(data: photoData) else {
// We are sending orginal file (not file compressed from memory).
guard let imageFileTranseferable = photoAttachment.imageFileTranseferable,
let data = try? Data(contentsOf: imageFileTranseferable.url),
let uiImage = UIImage(data: data) else {
return
}
guard let data = image.getJpegData() else {
// Compresing to JPEG with extendedRGB color space.
guard let data = uiImage.getJpegData() else {
return
}

View File

@ -502,9 +502,7 @@ struct ComposeView: View {
// Now we have to get from photos images as JPEG.
for item in self.photosAttachment.filter({ $0.photoData == nil }) {
if let data = try await item.loadData() {
item.photoData = data
}
try await item.loadImage()
}
// Open again the keyboard.
@ -535,16 +533,19 @@ struct ComposeView: View {
private func upload(_ photoAttachment: PhotoAttachment) async {
do {
// We have to have binary data and image shouldn't be uploaded yet.
guard let photoData = photoAttachment.photoData, photoAttachment.uploadedAttachment == nil else {
// Image shouldn't be uploaded yet.
guard photoAttachment.uploadedAttachment == nil else {
return
}
guard let image = UIImage(data: photoData) else {
// From extension we are sending already resized file.
guard let data = photoAttachment.photoData,
let uiImage = UIImage(data: data) else {
return
}
guard let data = image.getJpegData() else {
// Compresing to JPEG with extendedRGB color space.
guard let data = uiImage.getJpegData() else {
return
}

View File

@ -1,21 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
public extension NSItemProvider {
func loadData() async throws -> Data? {
return try await withCheckedThrowingContinuation { continuation in
_ = self.loadDataRepresentation(for: .image) { (data, error) in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: data)
}
}
}
}
}

View File

@ -0,0 +1,24 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftUI
public extension NSItemProvider {
func createImageFileTranseferable() async throws -> ImageFileTranseferable? {
return try await withCheckedThrowingContinuation { continuation in
_ = self.loadTransferable(type: ImageFileTranseferable.self) { result in
switch result {
case let .success(success):
continuation.resume(with: .success(success))
case .failure:
continuation.resume(with: .success(nil))
}
}
}
}
}

View File

@ -0,0 +1,24 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
import SwiftUI
import PhotosUI
public extension PhotosPickerItem {
func createImageFileTranseferable() async throws -> ImageFileTranseferable? {
return try await withCheckedThrowingContinuation { continuation in
_ = self.loadTransferable(type: ImageFileTranseferable.self) { result in
switch result {
case let .success(success):
continuation.resume(with: .success(success))
case .failure:
continuation.resume(with: .success(nil))
}
}
}
}
}

View File

@ -8,6 +8,7 @@ import SwiftUI
public extension UIImage {
func getJpegData() -> Data? {
#if targetEnvironment(simulator)
// For testing purposes.
let converted = self.convertToExtendedSRGBJpeg()

View File

@ -4,12 +4,11 @@
// Licensed under the Apache License 2.0.
//
import UIKit
import SwiftUI
import Foundation
public struct ImageFileTranseferable: Transferable {
let url: URL
lazy var data: Data? = try? Data(contentsOf: url)
public let url: URL
public static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .image) { image in

View File

@ -8,15 +8,27 @@ import Foundation
import PhotosUI
import SwiftUI
import PixelfedKit
import ServicesKit
public class PhotoAttachment: ObservableObject, Identifiable, Equatable, Hashable {
public let id: String
/// Information about image from photos picker.
public let photosPickerItem: PhotosPickerItem?
/// Information about image from share extension.
public let nsItemProvider: NSItemProvider?
/// Variable used for presentation layer.
@Published public var photoData: Data?
/// Property which stores orginal image file copied from Photos.
@Published public var imageFileTranseferable: ImageFileTranseferable?
/// Property stores information after upload to Pixelfed.
@Published public var uploadedAttachment: UploadedAttachment?
/// Error from Pixelfed.
@Published public var error: Error?
public init(photosPickerItem: PhotosPickerItem? = nil, nsItemProvider: NSItemProvider? = nil) {
@ -36,18 +48,20 @@ public class PhotoAttachment: ObservableObject, Identifiable, Equatable, Hashabl
}
public extension PhotoAttachment {
func loadData() async throws -> Data? {
@MainActor
func loadImage() async throws {
if let pickerItem = self.photosPickerItem,
let data = try await pickerItem.loadTransferable(type: Data.self) {
return data
let transferable = try await pickerItem.createImageFileTranseferable() {
self.imageFileTranseferable = transferable
self.photoData = await ImageCompressService.shared.compressImageFrom(url: transferable.url)
}
if let itemProvider = self.nsItemProvider,
let data = try await itemProvider.loadData() {
return data
let transferable = try await itemProvider.createImageFileTranseferable() {
self.imageFileTranseferable = transferable
self.photoData = await ImageCompressService.shared.compressImageFrom(url: transferable.url)
}
return nil
}
}