Last active
September 23, 2024 12:20
-
-
Save darrarski/ce912bef767c6b93582b63b12f946c31 to your computer and use it in GitHub Desktop.
SwiftUI emoji picker using UIKit on iOS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct EmojiPickerView: UIViewRepresentable { | |
@Binding var isFirstResponder: Bool | |
var onPick: (String) -> Void | |
var onDelete: () -> Void | |
func makeUIView(context: Context) -> UIViewType { | |
UIViewType(view: self) | |
} | |
func updateUIView(_ uiView: UIViewType, context: Context) { | |
DispatchQueue.main.async { | |
uiView.view = self | |
} | |
} | |
class UIViewType: UIView, UIKeyInput { | |
init(view: EmojiPickerView) { | |
self.view = view | |
super.init(frame: .zero) | |
} | |
required init?(coder: NSCoder) { nil } | |
var view: EmojiPickerView { | |
didSet { | |
if view.isFirstResponder && !isFirstResponder { | |
_ = becomeFirstResponder() | |
} | |
if !view.isFirstResponder && isFirstResponder { | |
_ = resignFirstResponder() | |
} | |
} | |
} | |
var hasText: Bool = true | |
override var canBecomeFirstResponder: Bool { true } | |
override var canResignFirstResponder: Bool { true } | |
override var textInputContextIdentifier: String? { "" } | |
override var textInputMode: UITextInputMode? { .emoji } | |
override func becomeFirstResponder() -> Bool { | |
let result = super.becomeFirstResponder() | |
if result && !view.isFirstResponder { | |
view.isFirstResponder = true | |
} | |
return result | |
} | |
override func resignFirstResponder() -> Bool { | |
let result = super.resignFirstResponder() | |
if result && view.isFirstResponder { | |
view.isFirstResponder = false | |
} | |
return result | |
} | |
func insertText(_ text: String) { | |
if text.containsOnlyEmoji { | |
view.onPick(text) | |
} else { | |
_ = resignFirstResponder() | |
} | |
} | |
func deleteBackward() { | |
view.onDelete() | |
} | |
} | |
} | |
extension UITextInputMode { | |
static var emoji: UITextInputMode? { | |
.activeInputModes.first { $0.primaryLanguage == "emoji" } | |
} | |
} | |
extension Character { | |
var isSimpleEmoji: Bool { | |
guard let firstScalar = unicodeScalars.first else { return false } | |
return firstScalar.properties.isEmoji && firstScalar.value > 0x238C | |
} | |
var isCombinedIntoEmoji: Bool { | |
unicodeScalars.count > 1 && unicodeScalars.first?.properties.isEmoji ?? false | |
} | |
var isEmoji: Bool { | |
isSimpleEmoji || isCombinedIntoEmoji | |
} | |
} | |
extension String { | |
var containsOnlyEmoji: Bool { | |
!isEmpty && allSatisfy(\.isEmoji) | |
} | |
} | |
struct EmojiPickerViewModifier: ViewModifier { | |
@Binding var isPresented: Bool | |
var onPick: (String) -> Void | |
var onDelete: () -> Void | |
func body(content: Content) -> some View { | |
let _ = isPresented | |
content.background { | |
EmojiPickerView( | |
isFirstResponder: $isPresented, | |
onPick: onPick, | |
onDelete: onDelete | |
) | |
} | |
} | |
} | |
extension View { | |
public func emojiPicker( | |
isPresented: Binding<Bool>, | |
onPick: @escaping (String) -> Void, | |
onDelete: @escaping () -> Void = {} | |
) -> some View { | |
modifier(EmojiPickerViewModifier( | |
isPresented: isPresented, | |
onPick: onPick, | |
onDelete: onDelete | |
)) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
public struct EmojiPickerExample: View { | |
public init() {} | |
@State var emoji: String? | |
@State var isPresentingPicker = false | |
public var body: some View { | |
VStack { | |
Text(emoji ?? "no emoji") | |
.font(.largeTitle) | |
HStack { | |
if isPresentingPicker { | |
Button("Hide Picker") { isPresentingPicker = false } | |
} else { | |
Button("Open Picker") { isPresentingPicker = true } | |
} | |
Button("Clear") { emoji = nil } | |
} | |
.buttonStyle(.borderedProminent) | |
.controlSize(.large) | |
} | |
.emojiPicker( | |
isPresented: $isPresentingPicker, | |
onPick: { emoji = $0 }, | |
onDelete: { emoji = nil } | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
EmojiPickerExample.mp4