Extract picked Memoji and stickers from UITextView (+SwiftUI compatible view)

Sometimes, you might want to allow the user to pick their Memoji and stickers and upload them within your own app.

To do that, you can present a keyboard and show Memojis and stickers. They will show on the keyboard if you have set the following properties for your UITextView:

textView.supportsAdaptiveImageGlyph = true
textView.allowsEditingTextAttributes = true

Then, you will set a UITextView delegate UITextViewDelegate to receive a call whenever the content changed.

textView.delegate = self

Conform the view controller to UITextViewDelegate. Then implement the textViewDidChange function

func textViewDidChange(_ textView: UITextView) {
    if let attachment = findFirstAttachment(in: textView.attributedText) {
        handleMemoji(attachment: attachment)
        textView.text = ""
        return
    }
}

First, we try to find the attachment object that has Memoji within. We first check by type adaptiveImageGlyph (only available for iOS 18 and up), which is usually the case when you pick a sticker within iOS 18 system. And we check for attachment too.

private func findFirstAttachment(in attributedText: NSAttributedString?) -> NSTextAttachment? {
    guard let attributedText else { return nil }
    
    // First try to find NSAdaptiveImageGlyph
    var foundGlyph: NSTextAttachment?
    attributedText.enumerateAttribute(.adaptiveImageGlyph,
                                      in: NSRange(location: 0, length: attributedText.length),
                                      options: []) { value, range, stop in
        if let glyph = value as? NSAdaptiveImageGlyph {
            let attachment = NSTextAttachment()
            attachment.image = UIImage(data: glyph.imageContent)
            foundGlyph = attachment
            stop.pointee = true
        }
    }
    
    if let foundGlyph { return foundGlyph }
    
    // Fallback to regular attachment
    var foundAttachment: NSTextAttachment?
    attributedText.enumerateAttribute(.attachment,
                                      in: NSRange(location: 0, length: attributedText.length),
                                      options: []) { value, range, stop in
        if let attachment = value as? NSTextAttachment {
            foundAttachment = attachment
            stop.pointee = true
        }
    }
    return foundAttachment
}

Then, if we have found such an text attachment, we extract the image:

private func handleMemoji(attachment: NSTextAttachment) {
    if let image = attachment.image {
        self.pickedImage = image
    } else if let image = attachment.image(forBounds: attachment.bounds,
                                           textContainer: nil,
                                           characterIndex: 0) {
        self.pickedImage = image
    } else if let imageData = attachment.fileWrapper?.regularFileContents,
              let image = UIImage(data: imageData) {
        self.pickedImage = image
    }
    
#if DEBUG
    print("Memoji attachment handled: \(String(describing: self.pickedImage))")
#endif
}

The below shows an example of a SwiftUI compatible view. If you are using UIKit, simple implement the delegate for your UITextView.

// MARK: - StickerPickingTextView
@available(iOS 18.0, *)
struct StickerPickingTextView: UIViewRepresentable {
    @Binding var pickedImage: UIImage?
    @Binding var pickedEmoji: String
    
    func makeUIView(context: Context) -> AdaptiveEmojiTextView {
        let textView = UITextView()
        textView.supportsAdaptiveImageGlyph = true
        textView.allowsEditingTextAttributes = true
        textView.delegate = context.coordinator
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) { return }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(pickedImage: $pickedImage, pickedEmoji: $pickedEmoji)
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        @Binding var pickedImage: UIImage?
        @Binding var pickedEmoji: String
        
        init(pickedImage: Binding<UIImage?>, pickedEmoji: Binding<String>) {
            self._pickedImage = pickedImage
            self._pickedEmoji = pickedEmoji
        }
        
        func textView(_ textView: UITextView,
                      shouldChangeTextIn range: NSRange,
                      replacementText text: String) -> Bool {
            let newLength = (textView.text?.count ?? 0) + text.count - range.length
            return newLength <= 1
        }
        
        func textViewDidChange(_ textView: UITextView) {
            // Handle Memoji and adaptive image glyphs
            if let attachment = findFirstAttachment(in: textView.attributedText) {
                handleMemoji(attachment: attachment)
                textView.text = ""
                return
            }
            
            // Handle regular emoji
            if let text = textView.text, !text.isEmpty {
                handleEmoji(text)
                textView.text = ""
            }
        }
        
        private func findFirstAttachment(in attributedText: NSAttributedString?) -> NSTextAttachment? {
            guard let attributedText else { return nil }
            
            // First try to find NSAdaptiveImageGlyph
            var foundGlyph: NSTextAttachment?
            attributedText.enumerateAttribute(.adaptiveImageGlyph,
                                              in: NSRange(location: 0, length: attributedText.length),
                                              options: []) { value, range, stop in
                if let glyph = value as? NSAdaptiveImageGlyph {
                    let attachment = NSTextAttachment()
                    attachment.image = UIImage(data: glyph.imageContent)
                    foundGlyph = attachment
                    stop.pointee = true
                }
            }
            
            if let foundGlyph { return foundGlyph }
            
            // Fallback to regular attachment
            var foundAttachment: NSTextAttachment?
            attributedText.enumerateAttribute(.attachment,
                                              in: NSRange(location: 0, length: attributedText.length),
                                              options: []) { value, range, stop in
                if let attachment = value as? NSTextAttachment {
                    foundAttachment = attachment
                    stop.pointee = true
                }
            }
            return foundAttachment
        }
        
        private func handleMemoji(attachment: NSTextAttachment) {
            if let image = attachment.image {
                self.pickedImage = image
            } else if let image = attachment.image(forBounds: attachment.bounds,
                                                   textContainer: nil,
                                                   characterIndex: 0) {
                self.pickedImage = image
            } else if let imageData = attachment.fileWrapper?.regularFileContents,
                      let image = UIImage(data: imageData) {
                self.pickedImage = image
            }
            
#if DEBUG
            print("Memoji attachment handled: \(String(describing: self.pickedImage))")
#endif
        }
        
        private func handleEmoji(_ text: String) {
            self.pickedEmoji = text
            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
                                            to: nil,
                                            from: nil,
                                            for: nil)
        }
    }
}