-
Notifications
You must be signed in to change notification settings - Fork 333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Loading an image with Styler #258
base: master
Are you sure you want to change the base?
Changes from all commits
1e19b5a
140ec8f
d7e62f9
a009e7e
3f852f3
ad2b541
2745a14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
// | ||
// AsyncImageLoad.swift | ||
// Down | ||
// | ||
// Created by Mikhail Ivanov on 03.06.2021. | ||
// Copyright © 2021 Down. All rights reserved. | ||
// | ||
|
||
#if canImport(UIKit) | ||
|
||
import UIKit | ||
|
||
#elseif canImport(AppKit) | ||
|
||
import AppKit | ||
|
||
#endif | ||
|
||
public protocol AsyncImageLoadDelegate | ||
{ | ||
func textAttachmentDidLoadImage(textAttachment: AsyncImageLoad, displaySizeChanged: Bool) | ||
} | ||
|
||
final public class AsyncImageLoad: NSTextAttachment | ||
{ | ||
public var imageURL: URL? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be optional? |
||
|
||
public var displaySize: CGSize? | ||
|
||
public var maximumDisplayWidth: CGFloat? | ||
|
||
public var delegate: AsyncImageLoadDelegate? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this delegate be �� |
||
|
||
weak var textContainer: NSTextContainer? | ||
|
||
private var originalImageSize: CGSize? | ||
|
||
public init(imageURL: URL? = nil, delegate: AsyncImageLoadDelegate? = nil) | ||
{ | ||
self.imageURL = imageURL | ||
self.delegate = delegate | ||
super.init(data: nil, ofType: nil) | ||
} | ||
|
||
required public init?(coder aDecoder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
#if os(macOS) | ||
override public var image: NSImage? { | ||
didSet { | ||
originalImageSize = image?.size | ||
} | ||
} | ||
#else | ||
override public var image: UIImage? { | ||
didSet { | ||
originalImageSize = image?.size | ||
} | ||
} | ||
#endif | ||
|
||
// MARK: - Function | ||
|
||
private func startAsyncImageDownload() | ||
{ | ||
guard let imageURL = imageURL, | ||
contents == nil | ||
else { | ||
return | ||
} | ||
|
||
URLSession.shared.dataTask(with: imageURL) { (data, response, error) in | ||
|
||
guard let data = data, | ||
error == nil else { | ||
print(error?.localizedDescription as Any) | ||
return | ||
} | ||
|
||
var displaySizeChanged = false | ||
|
||
self.contents = data | ||
|
||
#if os(macOS) | ||
if let image = NSImage(data: data) | ||
{ | ||
let imageSize = image.size | ||
|
||
if self.displaySize == nil | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the purpose of |
||
{ | ||
displaySizeChanged = true | ||
} | ||
|
||
self.originalImageSize = imageSize | ||
} | ||
#else | ||
if let image = UIImage(data: data) | ||
{ | ||
let imageSize = image.size | ||
|
||
if self.displaySize == nil | ||
{ | ||
displaySizeChanged = true | ||
} | ||
|
||
self.originalImageSize = imageSize | ||
} | ||
#endif | ||
|
||
DispatchQueue.main.async { | ||
if displaySizeChanged | ||
{ | ||
self.textContainer?.layoutManager?.setNeedsLayout(forAttachment: self) | ||
} | ||
else | ||
{ | ||
self.textContainer?.layoutManager?.setNeedsDisplay(forAttachment: self) | ||
} | ||
|
||
// notify the optional delegate | ||
self.delegate?.textAttachmentDidLoadImage(textAttachment: self, displaySizeChanged: displaySizeChanged) | ||
} | ||
|
||
}.resume() | ||
} | ||
|
||
#if os(macOS) | ||
public override func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> NSImage? | ||
{ | ||
if let image = image { return image } | ||
|
||
guard let contents = contents, let image = NSImage(data: contents) else | ||
{ | ||
self.textContainer = textContainer | ||
|
||
startAsyncImageDownload() | ||
|
||
return nil | ||
} | ||
|
||
return image | ||
} | ||
#else | ||
public override func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> UIImage? | ||
{ | ||
if let image = image { return image } | ||
|
||
guard let contents = contents, let image = UIImage(data: contents) else | ||
{ | ||
self.textContainer = textContainer | ||
|
||
startAsyncImageDownload() | ||
|
||
return nil | ||
} | ||
|
||
return image | ||
} | ||
#endif | ||
|
||
public override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect | ||
{ | ||
if let displaySize = displaySize | ||
{ | ||
return CGRect(origin: CGPoint.zero, size: displaySize) | ||
} | ||
|
||
if let imageSize = originalImageSize | ||
{ | ||
let maxWidth = maximumDisplayWidth ?? lineFrag.size.width | ||
let factor = maxWidth / imageSize.width | ||
|
||
return CGRect(origin: CGPoint.zero, size:CGSize(width: Int(imageSize.width * factor), height: Int(imageSize.height * factor))) | ||
} | ||
|
||
return CGRect.zero | ||
} | ||
} | ||
|
||
extension NSLayoutManager | ||
{ | ||
/// Determine the character ranges for an attachment | ||
private func rangesForAttachment(attachment: NSTextAttachment) -> [NSRange]? | ||
{ | ||
guard let attributedString = self.textStorage else | ||
{ | ||
return nil | ||
} | ||
|
||
// find character range for this attachment | ||
let range = NSRange(location: 0, length: attributedString.length) | ||
|
||
var refreshRanges = [NSRange]() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would we ever expect to find more than one range for a single attachment? |
||
|
||
attributedString.enumerateAttribute(NSAttributedString.Key.attachment, in: range, options: []) { (value, effectiveRange, nil) in | ||
|
||
guard let foundAttachment = value as? NSTextAttachment, foundAttachment == attachment else | ||
{ | ||
return | ||
} | ||
|
||
// add this range to the refresh ranges | ||
refreshRanges.append(effectiveRange) | ||
} | ||
|
||
if refreshRanges.count == 0 | ||
{ | ||
return nil | ||
} | ||
|
||
return refreshRanges | ||
} | ||
|
||
/// Trigger a relayout for an attachment | ||
public func setNeedsLayout(forAttachment attachment: NSTextAttachment) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be public? |
||
{ | ||
guard let ranges = rangesForAttachment(attachment: attachment) else | ||
{ | ||
return | ||
} | ||
|
||
// invalidate the display for the corresponding ranges | ||
for range in ranges.reversed() { | ||
self.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil) | ||
|
||
// also need to trigger re-display or already visible images might not get updated | ||
self.invalidateDisplay(forCharacterRange: range) | ||
} | ||
} | ||
|
||
/// Trigger a re-display for an attachment | ||
public func setNeedsDisplay(forAttachment attachment: NSTextAttachment) | ||
{ | ||
guard let ranges = rangesForAttachment(attachment: attachment) else | ||
{ | ||
return | ||
} | ||
|
||
// invalidate the display for the corresponding ranges | ||
for range in ranges.reversed() { | ||
self.invalidateDisplay(forCharacterRange: range) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,10 +40,12 @@ open class DownStyler: Styler { | |
.font: fonts.listItemPrefix, | ||
.foregroundColor: colors.listItemPrefix] | ||
} | ||
|
||
private var delegate: AsyncImageLoadDelegate? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Name suggestion: |
||
|
||
// MARK: - Life cycle | ||
|
||
public init(configuration: DownStylerConfiguration = DownStylerConfiguration()) { | ||
public init(configuration: DownStylerConfiguration = DownStylerConfiguration(), delegate: AsyncImageLoadDelegate? = nil) { | ||
fonts = configuration.fonts | ||
colors = configuration.colors | ||
paragraphStyles = configuration.paragraphStyles | ||
|
@@ -52,6 +54,7 @@ open class DownStyler: Styler { | |
codeBlockOptions = configuration.codeBlockOptions | ||
itemParagraphStyler = ListItemParagraphStyler(options: configuration.listItemOptions, | ||
prefixFont: fonts.listItemPrefix) | ||
self.delegate = delegate | ||
} | ||
|
||
// MARK: - Styling | ||
|
@@ -187,8 +190,9 @@ open class DownStyler: Styler { | |
} | ||
|
||
open func style(image str: NSMutableAttributedString, title: String?, url: String?) { | ||
guard let url = url else { return } | ||
styleGenericLink(in: str, url: url) | ||
guard let url = url, | ||
let urlImg = URL(string: url) else { return } | ||
Comment on lines
+193
to
+194
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: guard let imageURL = url.flatMap(URL.init) else { return } |
||
styleGenericImg(in: str, url: urlImg) | ||
} | ||
|
||
// MARK: - Common Styling | ||
|
@@ -218,6 +222,13 @@ open class DownStyler: Styler { | |
.link: url, | ||
.foregroundColor: colors.link]) | ||
} | ||
|
||
private func styleGenericImg(in str: NSMutableAttributedString, url: URL) { | ||
let image1Attachment = AsyncImageLoad(imageURL: url, delegate: delegate) | ||
let image1String = NSAttributedString(attachment: image1Attachment) | ||
Comment on lines
+227
to
+228
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Name suggestions: |
||
|
||
str.setAttributedString(image1String) | ||
} | ||
|
||
// MARK: - Helpers | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be public?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Name suggestion:
AsyncImageAttachment