Swift实现NSTextAttachment图片的scaleAspectFit适配需求
问题
需要为用作NSTextAttachment的图片实现类似UIView的contentMode = .scaleAspectFit适配效果,目前预设displaySize后,附件图片显示效果不符合预期,希望图片能按等比例缩放规则适配到设定的displaySize范围内,兼容不同尺寸的图片。
复现步骤
设置附件的displaySize为300x300,但图片无法按scaleAspectFit规则显示:
attachment = RemoteImageTextAttachment(imageURL: fileUrl!, displaySize: CGSize(width: 300, height: 300))
解决方案
要实现该效果,需修改RemoteImageTextAttachment的尺寸计算逻辑与边界返回规则,以下是完整修改后的代码:
public class RemoteImageTextAttachment: NSTextAttachment { // 附件所属的UILabel public weak var label: UILabel? // 图片的目标显示尺寸,为nil时使用图片原尺寸 public var displaySize: CGSize? public var downloadQueue: DispatchQueue? public let imageUrl: URL private weak var textContainer: NSTextContainer? private var isDownloading = false var imageCache = AutoPurgingImageCache() public init(imageURL: URL, displaySize: CGSize? = nil, downloadQueue: DispatchQueue? = nil) { self.imageUrl = imageURL self.displaySize = displaySize self.downloadQueue = downloadQueue super.init(data: nil, ofType: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 计算图片按aspect fit规则适配到目标尺寸后的实际大小 private func aspectFitSize(for imageSize: CGSize, within targetSize: CGSize) -> CGSize { guard imageSize.width > 0 && imageSize.height > 0 else { return targetSize } let widthRatio = targetSize.width / imageSize.width let heightRatio = targetSize.height / imageSize.height // 取最小缩放比例,保证图片完整显示在目标尺寸内 let scaleFactor = min(widthRatio, heightRatio) return CGSize(width: imageSize.width * scaleFactor, height: imageSize.height * scaleFactor) } override public func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { // 当有目标显示尺寸且图片已加载时,返回适配后的尺寸 if let displaySize = displaySize, let image = image { let fitSize = aspectFitSize(for: image.size, within: displaySize) return CGRect(origin: .zero, size: fitSize) } // 图片未加载但有目标尺寸时,先返回目标尺寸作为占位 if let displaySize = displaySize { return CGRect(origin: .zero, size: displaySize) } // 无目标尺寸时使用图片原尺寸 if let originalImageSize = image?.size { return CGRect(origin: .zero, size: originalImageSize) } // 图片未加载且无目标尺寸时的最小占位,保证触发图片下载逻辑 let placeholderSize = CGSize(width: 1, height: 1) return CGRect(origin: .zero, size: placeholderSize) } override public func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> UIImage? { // 优先从缓存取图 if let cachedImage = imageCache.image(withIdentifier: imageUrl.absoluteString) { self.image = cachedImage return cachedImage } if let image = image { return image } self.textContainer = textContainer guard !isDownloading else { return nil } isDownloading = true let imageUrl = self.imageUrl let downloadQueue = self.downloadQueue ?? DispatchQueue.global() downloadQueue.async { [weak self] in guard let data = try? Data(contentsOf: imageUrl) else { DispatchQueue.main.async { self?.isDownloading = false } return } guard let image = UIImage(data: data) else { DispatchQueue.main.async { self?.isDownloading = false } return } // 将下载的图片存入缓存 self?.imageCache.add(image, withIdentifier: imageUrl.absoluteString) DispatchQueue.main.async { [weak textContainer] in guard let strongSelf = self else { return } defer { strongSelf.isDownloading = false } strongSelf.image = image strongSelf.label?.setNeedsDisplay() // 更新UITextView/NSTextView的布局 if let layoutManager = strongSelf.textContainer?.layoutManager, let ranges = layoutManager.rangesForAttachment(strongSelf) { ranges.forEach { range in layoutManager.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil) } } } } return nil } } public extension NSLayoutManager { func rangesForAttachment(_ attachment: NSTextAttachment) -> [NSRange]? { guard let textStorage = textStorage else { return nil } var ranges: [NSRange] = [] textStorage.enumerateAttribute(.attachment, in: NSRange(location: 0, length: textStorage.length), options: [], using: { (attribute, range, _) in if let foundAttribute = attribute as? NSTextAttachment, foundAttribute === attachment { ranges.append(range) } }) return ranges } }
关键修改点
- 新增
aspectFitSize辅助方法:根据图片原尺寸和目标显示尺寸,计算等比例缩放后的实际大小,确保图片完整显示在目标尺寸内,无变形或超出边界。 - 调整
attachmentBounds逻辑:- 图片已加载且有目标尺寸时,返回适配后的尺寸作为附件显示边界
- 图片未加载时返回目标尺寸作为占位,提升体验
- 优化缓存与错误处理:利用现有
imageCache避免重复下载,修正原代码中下载失败时未重置isDownloading的问题。
内容的提问来源于stack exchange,提问作者husharoonie
相关产品推荐
相关产品推荐

