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)
}
}
}