diff --git a/Aztec/Classes/TextKit/ImageAttachment.swift b/Aztec/Classes/TextKit/ImageAttachment.swift index c89db97c8..44f2fc3b7 100644 --- a/Aztec/Classes/TextKit/ImageAttachment.swift +++ b/Aztec/Classes/TextKit/ImageAttachment.swift @@ -98,27 +98,27 @@ open class ImageAttachment: MediaAttachment { } override func onScreenHeight(_ containerWidth: CGFloat) -> CGFloat { - if let image = image { - let targetWidth = onScreenWidth(containerWidth) - let scale = targetWidth / image.size.width - - return floor(image.size.height * scale) + (appearance.imageMargin * 2) - } else { + guard let image = image else { return 0 } + + let targetWidth = onScreenWidth(containerWidth) + let scale = targetWidth / image.size.width + + return floor(image.size.height * scale) + (appearance.imageMargin * 2) } override func onScreenWidth(_ containerWidth: CGFloat) -> CGFloat { - if let image = image { - switch (size) { - case .full, .none: - return floor(min(image.size.width, containerWidth)) - default: - return floor(min(min(image.size.width,size.width), containerWidth)) - } - } else { + guard let image = image else { return 0 } + + switch (size) { + case .full, .none: + return floor(min(image.size.width, containerWidth)) + default: + return floor(min(min(image.size.width,size.width), containerWidth)) + } } } diff --git a/Aztec/Classes/TextKit/MediaAttachment.swift b/Aztec/Classes/TextKit/MediaAttachment.swift index 5e6e74881..5a354571f 100644 --- a/Aztec/Classes/TextKit/MediaAttachment.swift +++ b/Aztec/Classes/TextKit/MediaAttachment.swift @@ -11,7 +11,7 @@ protocol MediaAttachmentDelegate: class { onSuccess success: @escaping (UIImage) -> (), onFailure failure: @escaping () -> ()) - func mediaAttachmentPlaceholderImageFor(attachment: MediaAttachment) -> UIImage + func mediaAttachmentPlaceholder(for attachment: MediaAttachment) -> UIImage } // MARK: - MediaAttachment @@ -200,6 +200,13 @@ open class MediaAttachment: NSTextAttachment { return floor(min(image.size.width, containerWidth)) } + func mediaBounds(for bounds: CGRect) -> CGRect { + let origin = CGPoint(x: xPosition(forContainerWidth: bounds.width), y: appearance.imageMargin) + let size = CGSize(width: onScreenWidth(bounds.width), height: onScreenHeight(bounds.width) - appearance.imageMargin * 2) + + return CGRect(origin: origin, size: size) + } + // MARK: - NSTextAttachmentContainer @@ -208,7 +215,7 @@ open class MediaAttachment: NSTextAttachment { ensureImageIsUpToDate(in: textContainer) guard let image = image else { - return delegate!.mediaAttachmentPlaceholderImageFor(attachment: self) + return delegate!.mediaAttachmentPlaceholder(for: self) } if let cachedImage = glyphImage, imageBounds.size.equalTo(cachedImage.size) { @@ -220,127 +227,170 @@ open class MediaAttachment: NSTextAttachment { return glyphImage } - func mediaBounds(for bounds: CGRect) -> CGRect { - let containerWidth = bounds.size.width - let origin = CGPoint(x: xPosition(forContainerWidth: bounds.size.width), y: appearance.imageMargin) - let size = CGSize(width: onScreenWidth(containerWidth), height: onScreenHeight(containerWidth) - appearance.imageMargin * 2) - return CGRect(origin: origin, size: size) + /// Returns the "Onscreen Character Size" of the attachment range. When we're in Alignment.None, + /// the attachment will be 'Inline', and thus, we'll return the actual Associated View Size. + /// Otherwise, we'll always take the whole container's width. + /// + override open func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { + + ensureImageIsUpToDate(in: textContainer) + + if image == nil { + return .zero + } + + var padding = (textContainer?.lineFragmentPadding ?? 0) * 2 + if let storage = textContainer?.layoutManager?.textStorage, + let paragraphStyle = storage.attribute(.paragraphStyle, at: charIndex, effectiveRange: nil) as? NSParagraphStyle { + + let attachmentString = storage.attributedSubstring(from: NSMakeRange(charIndex, 1)).string + let headIndent = storage.string.isStartOfParagraph(at: attachmentString.startIndex) ? paragraphStyle.firstLineHeadIndent : paragraphStyle.headIndent + + padding += abs(paragraphStyle.tailIndent) + abs(headIndent) + } + + let width = floor(lineFrag.width - padding) + let size = CGSize(width: width, height: onScreenHeight(width)) + + return CGRect(origin: CGPoint.zero, size: size) } +} + + +// MARK: - Drawing Methods +// +extension MediaAttachment { - private func glyph(for image: UIImage, in bounds: CGRect) -> UIImage? { + /// Returns the Glyph representing the current image, with all of the required add-ons already embedded: + /// + /// - Overlay Background: Whenever there is a message (OR) upload in progress. + /// - Overlay Border: Whenever there is no upload in progress (OR) there is no message visible. + /// - Overlay Image: Image to be displayed at the center of the actual attached image + /// - OVerlay Message: Message to be displayed below the Overlay Image. + /// - Progress Bar: Whenever there's an Upload OP running. + /// + func glyph(for image: UIImage, in bounds: CGRect) -> UIImage? { UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0) let mediaBounds = self.mediaBounds(for: bounds) - let origin = mediaBounds.origin - let size = mediaBounds.size image.draw(in: mediaBounds) - drawOverlayBackground(at: origin, size: size) - drawOverlayBorder(at: origin, size: size) - drawProgress(at: origin, size: size) - - var imagePadding: CGFloat = 0 - if let overlayImage = overlayImage { - UIColor.white.set() - let sizeInsideBorder = CGSize(width: size.width - appearance.overlayBorderWidth, height: size.height - appearance.overlayBorderWidth) - let newImage = overlayImage.resizedImageWithinRect(rectSize: sizeInsideBorder, maxImageSize: overlayImage.size, color: UIColor.white) - let center = CGPoint(x: round(origin.x + (size.width / 2.0)), y: round(origin.y + (size.height / 2.0))) - newImage.draw(at: CGPoint(x: round(center.x - (newImage.size.width / 2.0)), y: round(center.y - (newImage.size.height / 2.0)))) - imagePadding += newImage.size.height - } + drawOverlayBackground(in: mediaBounds) + drawOverlayBorder(in: mediaBounds) - if let message = message { - let textRect = message.boundingRect(with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil) - var y = origin.y + ((size.height - textRect.height) / 2.0) - if imagePadding != 0 { - y = origin.y + Constants.messageTextTopMargin + ((size.height + imagePadding) / 2.0) - } - let textPosition = CGPoint(x: origin.x, y: y) + let overlayImageSize = drawOverlayImage(in: mediaBounds) + drawOverlayMessage(in: mediaBounds, paddingY: overlayImageSize.height) - // Check to see if the message will fit within the image. If not, skip it. - if (textPosition.y + textRect.height) < mediaBounds.height { - message.draw(in: CGRect(origin: textPosition, size: CGSize(width:size.width, height:textRect.size.height))) - } - } + drawProgress(in: mediaBounds) let result = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - return result; + + return result } - private func drawOverlayBackground(at origin: CGPoint, size:CGSize) { + + /// Draws an overlay on top of the image, with a color defined by the `appearance.overlayColor` property. + /// + private func drawOverlayBackground(in bounds: CGRect) { guard message != nil || progress != nil else { return } - let rect = CGRect(origin: origin, size: size) - let path = UIBezierPath(rect: rect) + + let path = UIBezierPath(rect: bounds) appearance.overlayColor.setFill() path.fill() } - private func drawOverlayBorder(at origin: CGPoint, size:CGSize) { - // Don't display the border if the border width is 0, we are force-hiding it, or message is set with no progress - guard appearance.overlayBorderWidth > 0, - shouldHideBorder == false, - progress == nil && message != nil else { - return + + /// Draws a border, surroinding the image. It's width will be defined by `appearance.overlayBorderWidth`, while it's color + /// will be taken from `appearance.overlayBorderColor`. + /// + /// Note that the `progress` is not nil, or there's an overlay message, this border will not be rendered. + /// + private func drawOverlayBorder(in bounds: CGRect) { + guard appearance.overlayBorderWidth > 0, shouldHideBorder == false, progress == nil, message != nil else { + return } - let rect = CGRect(origin: origin, size: size) - let path = UIBezierPath(rect: rect) + + let path = UIBezierPath(rect: bounds) appearance.overlayBorderColor.setStroke() - path.lineWidth = (appearance.overlayBorderWidth * 2.0) + path.lineWidth = appearance.overlayBorderWidth * 2.0 path.addClip() path.stroke() } - private func drawProgress(at origin: CGPoint, size:CGSize) { - guard let progress = progress else { - return + + /// Draws the overlayImage at the precise center of the Attachment's bounds. + /// + /// - Returns: The actual size of the overlayImage, once displayed onscreen. This size might be actually smaller than the one defined + /// by the actual asset, since we make sure not to render images bigger than the canvas. + /// + private func drawOverlayImage(in bounds: CGRect) -> CGSize { + guard let overlayImage = overlayImage else { + return .zero } - let lineY = origin.y + (appearance.progressHeight / 2.0) - let backgroundPath = UIBezierPath() - backgroundPath.lineWidth = appearance.progressHeight - appearance.progressBackgroundColor.setStroke() - backgroundPath.move(to: CGPoint(x:origin.x, y: lineY)) - backgroundPath.addLine(to: CGPoint(x: origin.x + size.width, y: lineY )) - backgroundPath.stroke() + UIColor.white.set() + let sizeInsideBorder = CGSize(width: bounds.width - appearance.overlayBorderWidth, height: bounds.height - appearance.overlayBorderWidth) + let resizedImage = overlayImage.resizedImageWithinRect(rectSize: sizeInsideBorder, maxImageSize: overlayImage.size, color: .white) - let path = UIBezierPath() - path.lineWidth = appearance.progressHeight - appearance.progressColor.setStroke() - path.move(to: CGPoint(x:origin.x, y: lineY)) - path.addLine(to: CGPoint(x: origin.x + (size.width * CGFloat(max(0,min(progress,1)))), y: lineY )) - path.stroke() + let overlayOrigin = CGPoint(x: round(bounds.midX - resizedImage.size.width * 0.5), + y: round(bounds.midY - resizedImage.size.height * 0.5)) + + resizedImage.draw(at: overlayOrigin) + + return resizedImage.size } - /// Returns the "Onscreen Character Size" of the attachment range. When we're in Alignment.None, - /// the attachment will be 'Inline', and thus, we'll return the actual Associated View Size. - /// Otherwise, we'll always take the whole container's width. + + /// Draws the Overlay's Message below the overlayImage, at the center of the Attachment. /// - override open func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { + private func drawOverlayMessage(in bounds: CGRect, paddingY: CGFloat) { + guard let message = message else { + return + } - ensureImageIsUpToDate(in: textContainer) + let textRect = message.boundingRect(with: bounds.size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil) + var messageY = bounds.minY + (bounds.height - textRect.height) * 0.5 - if image == nil { - return .zero + if paddingY != 0 { + messageY = bounds.minY + Constants.messageTextTopMargin + (bounds.height + paddingY) * 0.5 } - var padding = (textContainer?.lineFragmentPadding ?? 0) * 2 - if let storage = textContainer?.layoutManager?.textStorage, - let paragraphStyle = storage.attribute(.paragraphStyle, at: charIndex, effectiveRange: nil) as? NSParagraphStyle { - let attachmentString = storage.attributedSubstring(from: NSMakeRange(charIndex, 1)).string - let headIndent = storage.string.isStartOfParagraph(at: attachmentString.startIndex) ? paragraphStyle.firstLineHeadIndent : paragraphStyle.headIndent + // Check to see if the message will fit within the image. If not, skip it. + let messageRect = CGRect(x: bounds.minX, y: messageY, width: bounds.width, height: textRect.height) + if messageRect.maxY < bounds.height { + message.draw(in: messageRect) + } + } - padding += abs(paragraphStyle.tailIndent) + abs(headIndent) + + /// Draws a progress bar, at the top of the image, matching the percentage defined by the ivar `progress`. + /// + private func drawProgress(in bounds: CGRect) { + guard let progress = progress else { + return } - let width = floor(lineFrag.width - padding) - let size = CGSize(width: width, height: onScreenHeight(width)) + let progressY = bounds.minY + appearance.progressHeight * 0.5 + let progressWidth = bounds.width * CGFloat(max(0, min(progress, 1))) - return CGRect(origin: CGPoint.zero, size: size) + let backgroundPath = UIBezierPath() + backgroundPath.lineWidth = appearance.progressHeight + backgroundPath.move(to: CGPoint(x: bounds.minX, y: progressY)) + backgroundPath.addLine(to: CGPoint(x: bounds.maxX, y: progressY)) + appearance.progressBackgroundColor.setStroke() + backgroundPath.stroke() + + let progressPath = UIBezierPath() + progressPath.lineWidth = appearance.progressHeight + progressPath.move(to: CGPoint(x: bounds.minX, y: progressY)) + progressPath.addLine(to: CGPoint(x: bounds.minX + progressWidth, y: progressY)) + appearance.progressColor.setStroke() + progressPath.stroke() } } @@ -372,7 +422,7 @@ private extension MediaAttachment { return } - image = delegate!.mediaAttachmentPlaceholderImageFor(attachment: self) + image = delegate!.mediaAttachmentPlaceholder(for: self) isFetchingImage = true retryCount += 1 @@ -430,6 +480,7 @@ private extension MediaAttachment { /// Constants /// struct Constants { + /// Maximum number of times to retry downloading the asset, upon error /// static let maxRetryCount = 3 @@ -475,8 +526,7 @@ extension MediaAttachment { /// public var progressColor = UIColor.blue - /// The margin apply to the images being displayed. This is to avoid that two images in a row get - /// glued together. + /// The margin to apply to the images being displayed. This is to avoid that two images in a row get glued together. /// public var imageMargin = CGFloat(10.0) } diff --git a/Aztec/Classes/TextKit/TextStorage.swift b/Aztec/Classes/TextKit/TextStorage.swift index d51244589..8bff3df7d 100644 --- a/Aztec/Classes/TextKit/TextStorage.swift +++ b/Aztec/Classes/TextKit/TextStorage.swift @@ -429,7 +429,7 @@ private extension TextStorage { // extension TextStorage: MediaAttachmentDelegate { - func mediaAttachmentPlaceholderImageFor(attachment: MediaAttachment) -> UIImage { + func mediaAttachmentPlaceholder(for attachment: MediaAttachment) -> UIImage { guard let delegate = attachmentsDelegate else { fatalError() }