ios: Animated images (GIF) support (#1636)

* ios: Animated images (GIF) support

* Moved from String path to UIImage param

* Aspect ratio

* Image frame

* gif image size

* refactor

* refactor

* fix fullscreen scroll animation

* rename UploadContent -> AnyImage

* refactor, allow using gifs in profiles

* rename back

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko
2022-12-24 00:22:12 +03:00
committed by GitHub
parent 6205b03943
commit cd63f81292
13 changed files with 308 additions and 58 deletions
+1 -1
View File
@@ -1006,7 +1006,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.addChatItem(cInfo, cItem)
if let file = cItem.file,
let mc = cItem.content.msgContent,
file.fileSize <= MAX_IMAGE_SIZE {
file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV {
let acceptImages = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
if (mc.isImage && acceptImages)
|| (mc.isVoice && ((file.fileSize > MAX_VOICE_MESSAGE_SIZE_INLINE_SEND && acceptImages) || cInfo.chatType == .group)) {
@@ -0,0 +1,63 @@
//
// Created by Avently on 19.12.2022.
// Copyright (c) 2022 SimpleX Chat. All rights reserved.
//
import UIKit
import SwiftUI
class AnimatedImageView: UIView {
var image: UIImage? = nil
var imageView: UIImageView? = nil
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("Not implemented")
}
convenience init(image: UIImage) {
self.init()
self.image = image
imageView = UIImageView(gifImage: image)
imageView!.contentMode = .scaleAspectFit
self.addSubview(imageView!)
}
override func layoutSubviews() {
super.layoutSubviews()
imageView!.frame = bounds
}
func updateImage(_ image: UIImage) {
if let subview = self.subviews.first as? UIImageView {
if image.imageData != subview.gifImage?.imageData {
imageView = UIImageView(gifImage: image)
imageView!.contentMode = .scaleAspectFit
self.addSubview(imageView!)
subview.removeFromSuperview()
}
}
imageView!.frame = bounds
self.layoutSubviews()
}
}
struct SwiftyGif: UIViewRepresentable {
private let image: UIImage
init(image: UIImage) {
self.image = image
}
func makeUIView(context: Context) -> AnimatedImageView {
AnimatedImageView(image: image)
}
func updateUIView(_ imageView: AnimatedImageView, context: Context) {
imageView.updateImage(image)
imageView.imageView!.startAnimatingGif()
}
}
@@ -55,13 +55,19 @@ struct CIImageView: View {
}
private func imageView(_ img: UIImage) -> some View {
let w = img.size.width > img.size.height ? .infinity : maxWidth * 0.75
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : img.imageData == nil ? .infinity : maxWidth
DispatchQueue.main.async { imgWidth = w }
return ZStack(alignment: .topTrailing) {
Image(uiImage: img)
.resizable()
.scaledToFit()
.frame(maxWidth: w)
if img.imageData == nil {
Image(uiImage: img)
.resizable()
.scaledToFit()
.frame(maxWidth: w)
} else {
SwiftyGif(image: img)
.frame(width: w, height: w * img.size.height / img.size.width)
.scaledToFit()
}
loadingIndicator()
}
}
@@ -8,6 +8,7 @@
import SwiftUI
import SimpleXChat
import SwiftyGif
struct FullScreenImageView: View {
@EnvironmentObject var m: ChatModel
@@ -77,9 +78,14 @@ struct FullScreenImageView: View {
private func imageView(_ img: UIImage) -> some View {
ZStack {
Color.black
Image(uiImage: img)
.resizable()
.scaledToFit()
if img.imageData == nil {
Image(uiImage: img)
.resizable()
.scaledToFit()
} else {
SwiftyGif(image: img)
.scaledToFit()
}
}
}
+8 -5
View File
@@ -8,6 +8,7 @@
import SwiftUI
import SimpleXChat
import SwiftyGif
private let memberImageSize: CGFloat = 34
@@ -465,13 +466,15 @@ struct ChatView: View {
}
menu.append(shareUIAction())
menu.append(copyUIAction())
if let filePath = getLoadedFilePath(ci.file) {
if case .image = ci.content.msgContent, let image = UIImage(contentsOfFile: filePath) {
menu.append(saveImageAction(image))
} else {
if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) {
if image.imageData != nil, let filePath = getLoadedFilePath(ci.file) {
menu.append(saveFileAction(filePath))
} else {
menu.append(saveImageAction(image))
}
} else if case .file = ci.content.msgContent, let filePath = getLoadedFilePath(ci.file) {
menu.append(saveFileAction(filePath))
}
}
if ci.meta.editable && !mc.isVoice {
menu.append(editAction())
}
@@ -8,6 +8,8 @@
import SwiftUI
import SimpleXChat
import SwiftyGif
import PhotosUI
enum ComposePreview {
case noPreview
@@ -169,6 +171,37 @@ func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
return chatItemPreview
}
enum UploadContent: Equatable {
case simpleImage(image: UIImage)
case animatedImage(image: UIImage)
var uiImage: UIImage {
switch self {
case let .simpleImage(image): return image
case let .animatedImage(image): return image
}
}
static func loadFromURL(url: URL) -> UploadContent? {
do {
let data = try Data(contentsOf: url)
if let image = UIImage(data: data) {
try image.setGifFromData(data, levelOfIntegrity: 1.0)
logger.log("UploadContent: added animated image")
return .animatedImage(image: image)
} else { return nil }
} catch {
do {
if let image = try UIImage(data: Data(contentsOf: url)) {
logger.log("UploadContent: added simple image")
return .simpleImage(image: image)
}
} catch {}
}
return nil
}
}
struct ComposeView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var chat: Chat
@@ -183,7 +216,7 @@ struct ComposeView: View {
@State private var showChooseSource = false
@State private var showImagePicker = false
@State private var showTakePhoto = false
@State var chosenImages: [UIImage] = []
@State var chosenImages: [UploadContent] = []
@State private var showFileImporter = false
@State var chosenFile: URL? = nil
@@ -231,7 +264,7 @@ struct ComposeView: View {
},
finishVoiceMessageRecording: finishVoiceMessageRecording,
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
onImageAdded: { image in chosenImages = [image] },
onImagesAdded: { images in if !images.isEmpty { chosenImages = images }},
keyboardVisible: $keyboardVisible
)
.padding(.trailing, 12)
@@ -256,7 +289,15 @@ struct ComposeView: View {
}
if UIPasteboard.general.hasImages {
Button("Paste image") {
chosenImages = imageList(UIPasteboard.general.image)
UIPasteboard.general.itemProviders.forEach { p in
if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in
if let url = url, let image = UploadContent.loadFromURL(url: url) {
chosenImages.append(image)
}
}
}
}
}
}
Button("Choose file") {
@@ -283,7 +324,7 @@ struct ComposeView: View {
Task {
var imgs: [String] = []
for image in images {
if let img = resizeImageToStrSize(image, maxDataSize: 14000) {
if let img = resizeImageToStrSize(image.uiImage, maxDataSize: 14000) {
imgs.append(img)
await MainActor.run {
composeState = composeState.copy(preview: .imagePreviews(imagePreviews: imgs))
@@ -483,12 +524,12 @@ struct ComposeView: View {
case let .imagePreviews(imagePreviews: images):
let last = min(chosenImages.count, images.count) - 1
for i in 0..<last {
if let savedFile = saveImage(chosenImages[i]) {
if let savedFile = saveAnyImage(chosenImages[i]) {
_ = await send(.image(text: "", image: images[i]), quoted: nil, file: savedFile)
}
_ = try? await Task.sleep(nanoseconds: 100_000000)
}
if let savedFile = saveImage(chosenImages[last]) {
if let savedFile = saveAnyImage(chosenImages[last]) {
sent = await send(.image(text: msgText, image: images[last]), quoted: quoted, file: savedFile, live: live)
}
if sent == nil {
@@ -585,6 +626,13 @@ struct ComposeView: View {
return .text(msgText)
}
}
func saveAnyImage(_ img: UploadContent) -> String? {
switch img {
case let .simpleImage(image): return saveImage(image)
case let .animatedImage(image): return saveAnimImage(image)
}
}
}
private func startVoiceMessageRecording() async {
@@ -7,6 +7,9 @@
//
import SwiftUI
import SwiftyGif
import SimpleXChat
import PhotosUI
struct NativeTextEditor: UIViewRepresentable {
@Binding var text: String
@@ -14,7 +17,7 @@ struct NativeTextEditor: UIViewRepresentable {
let font: UIFont
@FocusState.Binding var focused: Bool
let alignment: TextAlignment
let onImageAdded: (UIImage) -> Void
let onImagesAdded: ([UploadContent]) -> Void
func makeUIView(context: Context) -> UITextView {
let field = CustomUITextField()
@@ -23,10 +26,10 @@ struct NativeTextEditor: UIViewRepresentable {
field.font = font
field.textAlignment = alignment == .leading ? .left : .right
field.autocapitalizationType = .sentences
field.setOnTextChangedListener { newText, image in
field.setOnTextChangedListener { newText, images in
text = newText
if let image = image {
onImageAdded(image)
if !images.isEmpty {
onImagesAdded(images)
}
}
field.setOnFocusChangedListener { focused = $0 }
@@ -43,33 +46,85 @@ struct NativeTextEditor: UIViewRepresentable {
}
private class CustomUITextField: UITextView, UITextViewDelegate {
var onTextChanged: (String, UIImage?) -> Void = { newText, image in }
var onTextChanged: (String, [UploadContent]) -> Void = { newText, image in }
var onFocusChanged: (Bool) -> Void = { focused in }
func setOnTextChangedListener(onTextChanged: @escaping (String, UIImage?) -> Void) {
func setOnTextChangedListener(onTextChanged: @escaping (String, [UploadContent]) -> Void) {
self.onTextChanged = onTextChanged
}
func setOnFocusChangedListener(onFocusChanged: @escaping (Bool) -> Void) {
self.onFocusChanged = onFocusChanged
}
func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
if !UIPasteboard.general.hasImages { return UIMenu(children: suggestedActions)}
return UIMenu(children: suggestedActions.map { elem in
if let elem = elem as? UIMenu {
var actions = elem.children
// Replacing Paste action since it allows to paste animated images too
let pasteIndex = elem.children.firstIndex { elem in elem.debugDescription.contains("Action: paste:")}
if let pasteIndex = pasteIndex {
let paste = actions[pasteIndex]
actions.remove(at: pasteIndex)
let newPaste = UIAction(title: paste.title, image: paste.image) { action in
var images: [UploadContent] = []
var totalImages = 0
var processed = 0
UIPasteboard.general.itemProviders.forEach { p in
if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
totalImages += 1
p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in
processed += 1
if let url = url, let image = UploadContent.loadFromURL(url: url) {
images.append(image)
DispatchQueue.main.sync {
self.onTextChanged(textView.text, images)
}
}
// No images were added, just paste a text then
if processed == totalImages && images.isEmpty {
textView.paste(UIPasteboard.general.string)
}
}
}
}
}
actions.insert(newPaste, at: 0)
}
return UIMenu(title: elem.title, subtitle: elem.subtitle, image: elem.image, identifier: elem.identifier, options: elem.options, children: actions)
} else {
return elem
}
})
}
func textViewDidChange(_ textView: UITextView) {
var image: UIImage? = nil
var images: [UploadContent] = []
var rangeDiff = 0
let newAttributedText = NSMutableAttributedString(attributedString: textView.attributedText)
textView.attributedText.enumerateAttribute(
NSAttributedString.Key.attachment,
in: NSRange(location: 0, length: textView.attributedText.length),
options: [],
using: { value, range, _ in
if let attachment = (value as? NSTextAttachment)?.image {
image = attachment
let newText = NSMutableAttributedString(attributedString: textView.attributedText)
newText.replaceCharacters(in: range, with: "")
textView.attributedText = newText
if let attachment = (value as? NSTextAttachment)?.fileWrapper?.regularFileContents {
do {
images.append(.animatedImage(image: try UIImage(gifData: attachment)))
} catch {
if let img = (value as? NSTextAttachment)?.image {
images.append(.simpleImage(image: img))
}
}
newAttributedText.replaceCharacters(in: NSMakeRange(range.location - rangeDiff, range.length), with: "")
rangeDiff += range.length
}
}
)
onTextChanged(textView.text, image)
if textView.attributedText != newAttributedText {
textView.attributedText = newAttributedText
}
onTextChanged(textView.text, images)
}
func textViewDidBeginEditing(_ textView: UITextView) {
@@ -90,7 +145,7 @@ struct NativeTextEditor_Previews: PreviewProvider{
font: UIFont.preferredFont(forTextStyle: .body),
focused: $keyboardVisible,
alignment: TextAlignment.leading,
onImageAdded: { _ in }
onImagesAdded: { _ in }
)
}
}
@@ -20,7 +20,7 @@ struct SendMessageView: View {
var startVoiceMessageRecording: (() -> Void)? = nil
var finishVoiceMessageRecording: (() -> Void)? = nil
var allowVoiceMessagesToContact: (() -> Void)? = nil
var onImageAdded: (UIImage) -> Void
var onImagesAdded: ([UploadContent]) -> Void
@State private var holdingVMR = false
@Namespace var namespace
@FocusState.Binding var keyboardVisible: Bool
@@ -66,7 +66,7 @@ struct SendMessageView: View {
font: teUiFont,
focused: $keyboardVisible,
alignment: alignment,
onImageAdded: onImageAdded
onImagesAdded: onImagesAdded
)
.allowsTightening(false)
.frame(height: teHeight)
@@ -314,7 +314,7 @@ struct SendMessageView_Previews: PreviewProvider {
SendMessageView(
composeState: $composeStateNew,
sendMessage: {},
onImageAdded: { _ in },
onImagesAdded: { _ in },
keyboardVisible: $keyboardVisible
)
}
@@ -324,7 +324,7 @@ struct SendMessageView_Previews: PreviewProvider {
SendMessageView(
composeState: $composeStateEditing,
sendMessage: {},
onImageAdded: { _ in },
onImagesAdded: { _ in },
keyboardVisible: $keyboardVisible
)
}
+26 -16
View File
@@ -8,28 +8,34 @@
import SwiftUI
import PhotosUI
import SwiftyGif
import SimpleXChat
struct LibraryImagePicker: View {
@Binding var image: UIImage?
var didFinishPicking: (_ didSelectItems: Bool) -> Void
@State var images: [UIImage] = []
@State var images: [UploadContent] = []
var body: some View {
LibraryImageListPicker(images: $images, selectionLimit: 1, didFinishPicking: didFinishPicking)
.onChange(of: images) { image = $0.first }
.onChange(of: images) { _ in
if let img = images.first {
image = img.uiImage
}
}
}
}
struct LibraryImageListPicker: UIViewControllerRepresentable {
typealias UIViewControllerType = PHPickerViewController
@Binding var images: [UIImage]
@Binding var images: [UploadContent]
var selectionLimit: Int
var didFinishPicking: (_ didSelectItems: Bool) -> Void
class Coordinator: PHPickerViewControllerDelegate {
let parent: LibraryImageListPicker
let dispatchQueue = DispatchQueue(label: "chat.simplex.app.LibraryImageListPicker")
var images: [UIImage] = []
var images: [UploadContent] = []
var imageCount: Int = 0
init(_ parent: LibraryImageListPicker) {
@@ -48,7 +54,11 @@ struct LibraryImageListPicker: UIViewControllerRepresentable {
for result in results {
logger.log("LibraryImageListPicker result")
let p = result.itemProvider
if p.canLoadObject(ofClass: UIImage.self) {
if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in
self.loadImage(object: url, error: error)
}
} else if p.canLoadObject(ofClass: UIImage.self) {
p.loadObject(ofClass: UIImage.self) { image, error in
DispatchQueue.main.async {
self.loadImage(object: image, error: error)
@@ -72,8 +82,10 @@ struct LibraryImageListPicker: UIViewControllerRepresentable {
if let error = error {
logger.error("LibraryImageListPicker: couldn't load image with error: \(error.localizedDescription)")
} else if let image = object as? UIImage {
images.append(image)
images.append(.simpleImage(image: image))
logger.log("LibraryImageListPicker: added image")
} else if let url = object as? URL, let image = UploadContent.loadFromURL(url: url) {
images.append(image)
}
dispatchQueue.sync {
self.imageCount -= 1
@@ -105,20 +117,18 @@ struct LibraryImageListPicker: UIViewControllerRepresentable {
}
struct CameraImageListPicker: View {
@Binding var images: [UIImage]
@Binding var images: [UploadContent]
@State var image: UIImage?
var body: some View {
CameraImagePicker(image: $image)
.onChange(of: image) { images = imageList($0) }
}
}
func imageList(_ img: UIImage?) -> [UIImage] {
if let img = img {
return [img]
} else {
return []
.onChange(of: image) { img in
if let img = img {
images = [UploadContent.simpleImage(image: img)]
} else {
images = []
}
}
}
}
+1 -1
View File
@@ -87,7 +87,7 @@ struct TerminalView: View {
composeState: $composeState,
sendMessage: sendMessage,
showVoiceMessageButton: false,
onImageAdded: { _ in },
onImagesAdded: { _ in },
keyboardVisible: $keyboardVisible
)
.padding(.horizontal, 12)
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
1841594C978674A7B42EF0C0 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841511920742C6E152E469F /* AnimatedImageView.swift */; };
3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C714776281C081000CB4D4B /* WebRTCView.swift */; };
3C71477A281C0F6800CB4D4B /* www in Resources */ = {isa = PBXBuildFile; fileRef = 3C714779281C0F6800CB4D4B /* www */; };
3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */; };
@@ -157,6 +158,8 @@
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
D77B92DE29523E1700A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DD29523E1700A5A1CC /* SwiftyGif */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -216,6 +219,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1841511920742C6E152E469F /* AnimatedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImageView.swift; sourceTree = "<group>"; };
3C714776281C081000CB4D4B /* WebRTCView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCView.swift; sourceTree = "<group>"; };
3C714779281C0F6800CB4D4B /* www */ = {isa = PBXFileReference; lastKnownFileType = folder; name = www; path = ../android/app/src/main/assets/www; sourceTree = "<group>"; };
3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteToConnectView.swift; sourceTree = "<group>"; };
@@ -382,6 +386,7 @@
buildActionMask = 2147483647;
files = (
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */,
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */,
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */,
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */,
);
@@ -408,6 +413,8 @@
files = (
5C70311F2955080A00150A12 /* libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
D77B92DE29523E1700A5A1CC /* SwiftyGif in Frameworks */,
5CBE6C2E2948E5B9002D9531 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C70311D2955080A00150A12 /* libgmp.a in Frameworks */,
5C70311E2955080A00150A12 /* libffi.a in Frameworks */,
@@ -693,6 +700,7 @@
5C7031152953C97F00150A12 /* CIFeaturePreferenceView.swift */,
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */,
644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */,
1841511920742C6E152E469F /* AnimatedImageView.swift */,
);
path = ChatItem;
sourceTree = "<group>";
@@ -771,6 +779,7 @@
name = "SimpleX (iOS)";
packageProductDependencies = (
5C8F01CC27A6F0D8007D2C8D /* CodeScanner */,
D77B92DB2952372200A5A1CC /* SwiftyGif */,
);
productName = "SimpleX (iOS)";
productReference = 5CA059CA279559F40002BEB4 /* SimpleX.app */;
@@ -826,6 +835,9 @@
dependencies = (
);
name = SimpleXChat;
packageProductDependencies = (
D77B92DD29523E1700A5A1CC /* SwiftyGif */,
);
productName = SimpleXChat;
productReference = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */;
productType = "com.apple.product-type.framework";
@@ -872,6 +884,7 @@
mainGroup = 5CA059BD279559F40002BEB4;
packageReferences = (
5C8F01CB27A6F0D8007D2C8D /* XCRemoteSwiftPackageReference "CodeScanner" */,
D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */,
);
productRefGroup = 5CA059CB279559F40002BEB4 /* Products */;
projectDirPath = "";
@@ -1040,6 +1053,7 @@
5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */,
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */,
644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */,
1841594C978674A7B42EF0C0 /* AnimatedImageView.swift in Sources */,
5C7031162953C97F00150A12 /* CIFeaturePreferenceView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1602,6 +1616,14 @@
minimumVersion = 2.0.0;
};
};
D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kirualex/SwiftyGif";
requirement = {
branch = master;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -1610,6 +1632,16 @@
package = 5C8F01CB27A6F0D8007D2C8D /* XCRemoteSwiftPackageReference "CodeScanner" */;
productName = CodeScanner;
};
D77B92DB2952372200A5A1CC /* SwiftyGif */ = {
isa = XCSwiftPackageProductDependency;
package = D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */;
productName = SwiftyGif;
};
D77B92DD29523E1700A5A1CC /* SwiftyGif */ = {
isa = XCSwiftPackageProductDependency;
package = D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */;
productName = SwiftyGif;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 5CA059BE279559F40002BEB4 /* Project object */;
@@ -8,6 +8,15 @@
"revision" : "c27a66149b7483fe42e2ec6aad61d5c3fffe522d",
"version" : "2.1.1"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif",
"state" : {
"branch" : "master",
"revision" : "4a6f5bad863c5365b192f8441f62c713ecff62bd"
}
}
],
"version" : 2
+20 -2
View File
@@ -9,12 +9,15 @@
import Foundation
import SwiftUI
import OSLog
import SwiftyGif
let logger = Logger()
// maximum image file size to be auto-accepted
public let MAX_IMAGE_SIZE: Int64 = 236700
public let MAX_IMAGE_SIZE_AUTO_RCV: Int64 = MAX_IMAGE_SIZE * 2
public let MAX_FILE_SIZE: Int64 = 8000000
public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(30)
@@ -182,8 +185,17 @@ public func getLoadedFilePath(_ file: CIFile?) -> String? {
}
public func getLoadedImage(_ file: CIFile?) -> UIImage? {
if let filePath = getLoadedFilePath(file) {
return UIImage(contentsOfFile: filePath)
let loadedFilePath = getLoadedFilePath(file)
if let loadedFilePath = loadedFilePath, let fileName = file?.filePath {
let filePath = getAppFilePath(fileName)
do {
let data = try Data(contentsOf: filePath)
let img = UIImage(data: data)
try img?.setGifFromData(data, levelOfIntegrity: 1.0)
return img
} catch {
return UIImage(contentsOfFile: loadedFilePath)
}
}
return nil
}
@@ -216,6 +228,12 @@ public func saveImage(_ uiImage: UIImage) -> String? {
return nil
}
public func saveAnimImage(_ image: UIImage) -> String? {
let fileName = generateNewFileName("IMG", "gif")
guard let imageData = image.imageData else { return nil }
return saveFile(imageData, fileName)
}
public func generateNewFileName(_ prefix: String, _ ext: String) -> String {
let timestamp = Date().getFormattedDate("yyyyMMdd_HHmmss")
let fileName = uniqueCombine("\(prefix)_\(timestamp).\(ext)")