diff --git a/RichEditorDemo/RichEditorDemo/RichEditorDemo.entitlements b/RichEditorDemo/RichEditorDemo/RichEditorDemo.entitlements index f2ef3ae..625af03 100644 --- a/RichEditorDemo/RichEditorDemo/RichEditorDemo.entitlements +++ b/RichEditorDemo/RichEditorDemo/RichEditorDemo.entitlements @@ -2,9 +2,11 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + diff --git a/Sources/RichEditorSwiftUI/Actions/RichTextAction.swift b/Sources/RichEditorSwiftUI/Actions/RichTextAction.swift index b166383..1347ce8 100644 --- a/Sources/RichEditorSwiftUI/Actions/RichTextAction.swift +++ b/Sources/RichEditorSwiftUI/Actions/RichTextAction.swift @@ -80,6 +80,10 @@ public enum RichTextAction: Identifiable, Equatable { /// Set link case setLink(String? = nil) + + /// Update Image Attachment as image takes time to download + case updateImageAttachments([ImageAttachment]) + } extension RichTextAction { @@ -114,6 +118,7 @@ extension RichTextAction { case .undoLatestChange: .richTextUndo case .setHeaderStyle: .richTextIgnoreIt case .setLink: .richTextLink + case .updateImageAttachments: .richTextIgnoreIt } } @@ -164,7 +169,7 @@ extension RichTextAction { case .toggleStyle(let style): style.titleKey case .undoLatestChange: .actionUndoLatestChange case .setLink: .link - case .setHeaderStyle: .ignoreIt + case .setHeaderStyle, .updateImageAttachments: .ignoreIt } } } diff --git a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift index f2aae8b..ac07060 100644 --- a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift +++ b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift @@ -76,6 +76,10 @@ import Foundation } else { removeLink() } + case .updateImageAttachments(let attachments): + attachments.forEach({ + textView.setImageAttachment(imageAttachment: $0) + }) } } } diff --git a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift index c69e086..39498b2 100644 --- a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift +++ b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift @@ -74,6 +74,7 @@ super.init() self.textView.delegate = self subscribeToUserActions() + context.onTextViewDidEndWithSetUp() } #if canImport(UIKit) diff --git a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Pasting.swift b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Pasting.swift index 173dc47..d357e29 100644 --- a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Pasting.swift +++ b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Pasting.swift @@ -76,7 +76,7 @@ extension RichTextViewComponent { let isSelectedRange = (index == selectedRange.location) if isSelectedRange { deleteCharacters(in: selectedRange) } if move { moveInputCursor(to: index) } - var insertedString: NSMutableAttributedString = .init() + let insertedString: NSMutableAttributedString = .init() images.reversed().forEach { insertedString.append(performPasteImage($0, at: index) ?? .init()) } @@ -88,6 +88,12 @@ extension RichTextViewComponent { #endif } + public func setImageAttachment(imageAttachment: ImageAttachment) { + guard let range = imageAttachment.range else { return } + let image = imageAttachment.image + performSetImageAttachment(image, at: range) + } + /** Paste text into the text view, at a certain index. @@ -154,3 +160,15 @@ extension RichTextViewComponent { } } #endif + +#if iOS || macOS || os(tvOS) || os(visionOS) + extension RichTextViewComponent { + fileprivate func performSetImageAttachment( + _ image: ImageRepresentable, + at range: NSRange + ) { + guard let attachmentString = getAttachmentString(for: image) else { return } + mutableAttributedString?.replaceCharacters(in: range, with: attachmentString) + } + } +#endif diff --git a/Sources/RichEditorSwiftUI/Data/Models/RichAttributes+ImageDownload.swift b/Sources/RichEditorSwiftUI/Data/Models/RichAttributes+ImageDownload.swift new file mode 100644 index 0000000..39ee4a5 --- /dev/null +++ b/Sources/RichEditorSwiftUI/Data/Models/RichAttributes+ImageDownload.swift @@ -0,0 +1,18 @@ +// +// RichAttributes+ImageDownload.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 31/12/24. +// + +import Foundation + +extension RichAttributes { + func getImage() async -> ImageRepresentable? { + if let imageUrl = image { + let image = try? await ImageDownloadManager.shared.fetchImage(from: imageUrl) + return image + } + return nil + } +} diff --git a/Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift b/Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift index 70ad0d5..07c8d73 100644 --- a/Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift +++ b/Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift @@ -222,8 +222,10 @@ extension RichAttributes { ? (byAdding ? att.align! : nil) : self.align), ///nil link indicates removal as well so removing link if `byAdding == false && att.link == nil` link: (att.link != nil - ? (byAdding ? att.link! : nil) : (att.link == nil && !byAdding) ? nil : self.link), - image: (att.image != nil ? (byAdding ? att.image! : nil) : self.image) + ? (byAdding ? att.link! : nil) + : (att.link == nil && !byAdding) ? nil : self.link), + image: (att.image != nil + ? (byAdding ? att.image! : nil) : self.image) ) } } diff --git a/Sources/RichEditorSwiftUI/Images/ImageAttachment.swift b/Sources/RichEditorSwiftUI/Images/ImageAttachment.swift index 9c6d75e..4abe503 100644 --- a/Sources/RichEditorSwiftUI/Images/ImageAttachment.swift +++ b/Sources/RichEditorSwiftUI/Images/ImageAttachment.swift @@ -7,7 +7,11 @@ import Foundation -public class ImageAttachment { +public class ImageAttachment: Equatable { + public static func == (lhs: ImageAttachment, rhs: ImageAttachment) -> Bool { + return lhs.id == rhs.id && lhs.image == rhs.image + } + public let id: String public let image: ImageRepresentable internal var range: NSRange? = nil diff --git a/Sources/RichEditorSwiftUI/Images/ImageDownloadManager.swift b/Sources/RichEditorSwiftUI/Images/ImageDownloadManager.swift index 264111c..21d5791 100644 --- a/Sources/RichEditorSwiftUI/Images/ImageDownloadManager.swift +++ b/Sources/RichEditorSwiftUI/Images/ImageDownloadManager.swift @@ -57,8 +57,7 @@ public class ImageDownloadManager { userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]) } - let (data, _) = try await URLSession.shared.data(from: url) - + let (data, response) = try await URLSession.shared.data(from: url) guard let image = ImageRepresentable(data: data) else { throw NSError( domain: "ImageDownloadManager", code: 500, diff --git a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift index 7b6c850..16dcc97 100644 --- a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift +++ b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift @@ -232,6 +232,69 @@ public class RichEditorState: ObservableObject { } } +//MARK: - Handle Image download +extension RichEditorState { + func onTextViewDidEndWithSetUp() { + setupWithImage() + } + + func setupWithImage() { + let imageSpans = internalSpans.filter({ $0.attributes?.image != nil }) + guard !imageSpans.isEmpty else { return } + imageSpans.forEach { item in + Task { @MainActor [weak self] in + guard let attributes = item.attributes else { return } + let image = await attributes.getImage() + if let image, let imageUrl = attributes.image { + let attachment = ImageAttachment(image: image, url: imageUrl) + attachment.updateRange(with: item.spanRange) + self?.actionPublisher.send(.updateImageAttachments([attachment])) + } + } + } + } + + func setupWithImageAttachment(imageAttachment: [ImageAttachment]) { + let richText = internalRichText + var tempSpans: [RichTextSpanInternal] = [] + var text = "" + richText.spans.forEach({ + let span = RichTextSpanInternal( + from: text.utf16Length, + to: (text.utf16Length + $0.insert.utf16Length - 1), + attributes: $0.attributes) + + tempSpans.append(span) + text += $0.insert + }) + + let str = NSMutableAttributedString(string: text) + + tempSpans.forEach { span in + str.addAttributes( + span.attributes?.toAttributes(font: .standardRichTextFont) + ?? [:], range: span.spanRange) + if span.attributes?.color == nil { + var color: ColorRepresentable = .clear + #if os(watchOS) + color = .black + #else + color = RichTextView.Theme.standard.fontColor + #endif + str.addAttributes( + [.foregroundColor: color], range: span.spanRange) + } + if let imageUrl = span.attributes?.image, + let image = imageAttachment.first(where: { $0.url == imageUrl }) + { + str.addAttribute(.attachment, value: image.image, range: span.spanRange) + } + } + + self.attributedString = str + } +} + extension RichEditorState { /// Whether or not the context has a selected range.