forked from shadowfacts/Tusker
Improve emoji loading behavior
Use transparent placeholders to prevent wrong initial layout when some or all emojis aren't cached.
This commit is contained in:
parent
1c0291b1dd
commit
36a78f1a3c
|
@ -17,13 +17,21 @@ extension NSTextAttachment {
|
|||
let defaultScale: CGFloat = 1.4
|
||||
imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * defaultScale, height: imageSizeMatchingFontSize.height * defaultScale)
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(imageSizeMatchingFontSize, false, 0.0)
|
||||
let attachmentImage = UIGraphicsImageRenderer(size: imageSizeMatchingFontSize).image { (_) in
|
||||
textColor.set()
|
||||
image.draw(in: CGRect(origin: .zero, size: imageSizeMatchingFontSize))
|
||||
let attachmentImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
}
|
||||
|
||||
self.init()
|
||||
self.image = attachmentImage
|
||||
}
|
||||
|
||||
convenience init(emojiPlaceholderIn font: UIFont) {
|
||||
let adjustedCapHeight = font.capHeight - 1
|
||||
// assumes emoji are mostly square
|
||||
let size = CGSize(width: adjustedCapHeight, height: adjustedCapHeight)
|
||||
let image = UIGraphicsImageRenderer(size: size).image { (_) in }
|
||||
self.init()
|
||||
self.image = image
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,19 +19,28 @@ protocol BaseEmojiLabel: AnyObject {
|
|||
}
|
||||
|
||||
extension BaseEmojiLabel {
|
||||
func replaceEmojis(in attributedString: NSAttributedString, emojis: [Emoji], identifier: String?, completion: @escaping (_ attributedString: NSAttributedString, _ didReplaceEmojis: Bool) -> Void) {
|
||||
func replaceEmojis(in attributedString: NSAttributedString, emojis: [Emoji], identifier: String?, setAttributedString: @escaping (_ attributedString: NSAttributedString, _ didReplaceEmojis: Bool) -> Void) {
|
||||
// blergh
|
||||
precondition(Thread.isMainThread)
|
||||
|
||||
emojiRequests.forEach { $0.cancel() }
|
||||
emojiRequests = []
|
||||
|
||||
guard !emojis.isEmpty else {
|
||||
completion(attributedString, false)
|
||||
setAttributedString(attributedString, false)
|
||||
return
|
||||
}
|
||||
|
||||
let matches = emojiRegex.matches(in: attributedString.string, options: [], range: NSRange(location: 0, length: attributedString.length))
|
||||
guard !matches.isEmpty else {
|
||||
completion(attributedString, false)
|
||||
setAttributedString(attributedString, false)
|
||||
return
|
||||
}
|
||||
|
||||
let emojiImages = MultiThreadDictionary<String, UIImage>(name: "BaseEmojiLabel Emoji Images")
|
||||
// not using a MultiThreadDictionary so that cached images can be added immediately
|
||||
// without jumping through various queues so that we can use them immediately
|
||||
// in building either the final string or the string with placeholders
|
||||
var emojiImages: [String: UIImage] = [:]
|
||||
var foundEmojis = false
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
@ -47,47 +56,83 @@ extension BaseEmojiLabel {
|
|||
|
||||
foundEmojis = true
|
||||
|
||||
if let image = ImageCache.emojis.get(emoji.url)?.image {
|
||||
// if the image is cached, add it immediately
|
||||
emojiImages[emoji.shortcode] = image
|
||||
} else {
|
||||
// otherwise, perform the network request
|
||||
|
||||
group.enter()
|
||||
// todo: ImageCache.emojis.get here will re-check the memory and disk caches, there should be another method to force-refetch
|
||||
let request = ImageCache.emojis.get(emoji.url) { (_, image) in
|
||||
defer { group.leave() }
|
||||
guard let image = image,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: emoji.url, image: image) else {
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
// sync back to the main thread to add the dictionary
|
||||
// todo: using the main thread for this isn't great
|
||||
DispatchQueue.main.async {
|
||||
emojiImages[emoji.shortcode] = transformedImage
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
if let request = request {
|
||||
emojiRequests.append(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard foundEmojis else {
|
||||
completion(attributedString, false)
|
||||
setAttributedString(attributedString, false)
|
||||
return
|
||||
}
|
||||
|
||||
func buildStringWithEmojisReplaced(usePlaceholders: Bool) -> NSAttributedString {
|
||||
let mutAttrString = NSMutableAttributedString(attributedString: attributedString)
|
||||
|
||||
// replaces the emojis starting from the end of the string as to not alter the indices of preceeding emojis
|
||||
for match in matches.reversed() {
|
||||
let shortcode = (attributedString.string as NSString).substring(with: match.range(at: 1))
|
||||
let attachment: NSTextAttachment
|
||||
|
||||
if let emojiImage = emojiImages[shortcode] {
|
||||
attachment = NSTextAttachment(emojiImage: emojiImage, in: self.emojiFont, with: self.emojiTextColor)
|
||||
} else if usePlaceholders {
|
||||
attachment = NSTextAttachment(emojiPlaceholderIn: self.emojiFont)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
let attachmentStr = NSAttributedString(attachment: attachment)
|
||||
mutAttrString.replaceCharacters(in: match.range, with: attachmentStr)
|
||||
}
|
||||
|
||||
return mutAttrString
|
||||
}
|
||||
|
||||
if emojiRequests.isEmpty {
|
||||
// waiting on the group when there are only cached emojis results in a bad first layout,
|
||||
// because it would still wait until the next runloop iteration
|
||||
// but, since there were no requests, we can generate the correct string immediately
|
||||
let replacedString = buildStringWithEmojisReplaced(usePlaceholders: false)
|
||||
setAttributedString(replacedString, true)
|
||||
} else {
|
||||
// if we did find emojis and there are pending network requests,
|
||||
// set an attributed strings with placeholders for non-loaded emojis to avoid layout shift
|
||||
setAttributedString(buildStringWithEmojisReplaced(usePlaceholders: true), true)
|
||||
|
||||
group.notify(queue: .main) { [weak self] in
|
||||
// if e.g. the account changes before all emojis are loaded, don't bother trying to set them
|
||||
guard let self = self, self.emojiIdentifier == identifier else { return }
|
||||
|
||||
let mutAttrString = NSMutableAttributedString(attributedString: attributedString)
|
||||
// replaces the emojis starting from the end of the string as to not alter the indices of preceeding emojis
|
||||
for match in matches.reversed() {
|
||||
let shortcode = (attributedString.string as NSString).substring(with: match.range(at: 1))
|
||||
guard let emojiImage = emojiImages[shortcode] else {
|
||||
continue
|
||||
let replacedString = buildStringWithEmojisReplaced(usePlaceholders: false)
|
||||
setAttributedString(replacedString, true)
|
||||
}
|
||||
|
||||
let attachment = NSTextAttachment(emojiImage: emojiImage, in: self.emojiFont, with: self.emojiTextColor)
|
||||
let attachmentStr = NSAttributedString(attachment: attachment)
|
||||
mutAttrString.replaceCharacters(in: match.range, with: attachmentStr)
|
||||
}
|
||||
|
||||
completion(mutAttrString, true)
|
||||
}
|
||||
}
|
||||
|
||||
func replaceEmojis(in string: String, emojis: [Emoji], identifier: String?, completion: @escaping (_ attributedString: NSAttributedString, _ didReplaceEmojis: Bool) -> Void) {
|
||||
replaceEmojis(in: NSAttributedString(string: string), emojis: emojis, identifier: identifier, completion: completion)
|
||||
func replaceEmojis(in string: String, emojis: [Emoji], identifier: String?, setAttributedString: @escaping (_ attributedString: NSAttributedString, _ didReplaceEmojis: Bool) -> Void) {
|
||||
replaceEmojis(in: NSAttributedString(string: string), emojis: emojis, identifier: identifier, setAttributedString: setAttributedString)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue