Change loading images in compose screen
This commit is contained in:
parent
90222ca16b
commit
02871e07a0
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import SwiftUI
|
|||
|
||||
public extension UIImage {
|
||||
func getJpegData() -> Data? {
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
// For testing purposes.
|
||||
let converted = self.convertToExtendedSRGBJpeg()
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue