Created
December 30, 2025 18:11
-
-
Save AbodiDawoud/7dcc0fdb5de72e7ef5282e457b6bd6d1 to your computer and use it in GitHub Desktop.
Custom Emoji Picker View
This file contains hidden or 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 | |
| @main | |
| struct EmojiPickerDemoApp: App { | |
| @State private var selectedEmoji: String = "🏴☠️" | |
| @State private var showEmojiPopover: Bool = false | |
| var body: some Scene { | |
| WindowGroup { | |
| ZStack { | |
| VisualEffectBlur(material: .hudWindow, blendingMode: .behindWindow) | |
| .ignoresSafeArea() | |
| demoView | |
| } | |
| .frame(width: 275, height: 130, alignment: .center) | |
| } | |
| .windowResizability(.contentSize) | |
| .windowStyle(.hiddenTitleBar) | |
| } | |
| private var demoView: some View { | |
| HStack { | |
| Button("Select Emoji") { | |
| showEmojiPopover = true | |
| } | |
| .fontWeight(.semibold) | |
| .buttonStyle(.plain) | |
| Divider() | |
| .opacity(0.8) | |
| .padding(.vertical, 1.4).padding(.horizontal, 4) | |
| Text(selectedEmoji) | |
| .fixedSize(horizontal: true, vertical: false) | |
| } | |
| .padding(.horizontal, 20) | |
| .padding(.vertical, 8) | |
| .fixedSize(horizontal: false, vertical: true) | |
| .overlay { | |
| RoundedRectangle(cornerRadius: 8) | |
| .stroke(.quaternary, lineWidth: 1) | |
| } | |
| .emojiPalette(selectedEmoji: $selectedEmoji, isPresented: $showEmojiPopover) | |
| } | |
| } | |
| struct EmojiPaletteView: View { | |
| @Binding var selectedEmoji: String | |
| @State private var selectedCategory: String = EmojiProvider.recentsKey | |
| private let columns: [GridItem] = Array(repeating: .init(.flexible(), spacing: 13), count: 7) | |
| private let provider = EmojiProvider.shared | |
| var body: some View { | |
| NavigationStack { | |
| List { | |
| LazyVGrid(columns: columns, spacing: 8) { | |
| ForEach(provider.emojisFromCategory(selectedCategory), id: \.self) { emoji in | |
| Button(emoji) { | |
| selectedEmoji = emoji | |
| print(selectedEmoji + ": " + selectedCategory) | |
| } | |
| .font(.system(size: 26)) | |
| .buttonStyle(.borderless) | |
| .frame(width: 32, height: 32) | |
| } | |
| } | |
| .listRowSeparator(.hidden) | |
| .id(selectedCategory) | |
| } | |
| .listStyle(.plain) | |
| .navigationTitle(provider.categoryLocalizedName(for: selectedCategory)) | |
| .toolbarTitleDisplayMode(.inline) | |
| .toolbarBackgroundVisibility(.visible, for: .windowToolbar) | |
| .safeAreaInset(edge: .bottom) { | |
| HStack(spacing: 8) { | |
| ForEach(provider.categories, id: \.self) { category in | |
| Image(systemName: provider.getSFSymbol(for: category)) | |
| .font(.system(size: 18)) | |
| .frame(width: 24, height: 24) | |
| .foregroundColor(category == selectedCategory ? Color.accentColor : .secondary) | |
| .onTapGesture { | |
| selectedCategory = category | |
| } | |
| } | |
| } | |
| .padding(8) | |
| .background(.bar) | |
| } | |
| } | |
| .frame(width: 300, height: 300) | |
| } | |
| } | |
| extension View { | |
| /// Presents an emoji picker in a popover modifier | |
| func emojiPalette(selectedEmoji: Binding<String>, | |
| isPresented: Binding<Bool>, | |
| attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), | |
| arrowEdge: Edge = .top | |
| ) -> some View { | |
| self.popover(isPresented: isPresented, attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge) { | |
| EmojiPaletteView(selectedEmoji: selectedEmoji).presentationCompactAdaptation(.popover) | |
| } | |
| } | |
| } | |
| struct VisualEffectBlur: NSViewRepresentable { | |
| var material: NSVisualEffectView.Material | |
| var blendingMode: NSVisualEffectView.BlendingMode | |
| func makeNSView(context: Context) -> NSVisualEffectView { | |
| let view = NSVisualEffectView() | |
| view.material = material | |
| view.blendingMode = blendingMode | |
| view.state = .active | |
| return view | |
| } | |
| func updateNSView(_ nsView: NSVisualEffectView, context: Context) { | |
| nsView.material = material | |
| nsView.blendingMode = blendingMode | |
| } | |
| } | |
| class EmojiProvider { | |
| static let shared = EmojiProvider() | |
| var categories: [String] { | |
| let EMFEmojiCategory = NSClassFromString("EMFEmojiCategory") as! NSObject.Type | |
| let list = EMFEmojiCategory.value(forKey: "categoryIdentifierList") as! [String] | |
| return list | |
| } | |
| func categoryLocalizedName(for identifier: String) -> String { | |
| let category = categoryWithIdentifier(identifier) | |
| let localizedName = category.value(forKey: "localizedName") as! String | |
| return localizedName | |
| } | |
| func emojisFromCategory(_ identifier: String) -> [String] { | |
| let category = categoryWithIdentifier(identifier) | |
| let tokens = category.perform( | |
| NSSelectorFromString("emojiTokensForLocaleData:"), with: nil | |
| ).takeUnretainedValue() as! [NSObject] | |
| let tokensAsStrings: [String] = tokens.compactMap { | |
| $0.value(forKey: "string") as? String | |
| } | |
| return tokensAsStrings | |
| } | |
| func getSFSymbol(for identifier: String) -> String { | |
| switch identifier { | |
| case "EMFEmojiCategoryPeople": return "face.smiling" | |
| case "EMFEmojiCategoryNature": return "teddybear" | |
| case "EMFEmojiCategoryFoodAndDrink": return "fork.knife" | |
| case "EMFEmojiCategoryActivity": return "basketball" | |
| case "EMFEmojiCategoryTravelAndPlaces": return "car" | |
| case "EMFEmojiCategoryObjects": return "lightbulb" | |
| case "EMFEmojiCategorySymbols": return "music.note" | |
| case "EMFEmojiCategoryFlags": return "flag" | |
| default: return "clock" | |
| } | |
| } | |
| static var recentsKey: String { | |
| return "EMFEmojiCategoryRecents" | |
| } | |
| } | |
| // Thanks to pookjw: https://github.com/pookjw/EmojiExplorer | |
| fileprivate func categoryWithIdentifier(_ identifier: String) -> AnyObject { | |
| let handle = dlopen("/System/Library/PrivateFrameworks/EmojiFoundation.framework/EmojiFoundation", RTLD_NOW)! | |
| let symbol = identifier.withCString { ptr in | |
| dlsym(handle, ptr) | |
| }! | |
| let identifierPtr = symbol.assumingMemoryBound(to: AnyObject.self) | |
| let EMFEmojiCategory: AnyClass = objc_lookUpClass("EMFEmojiCategory")! | |
| let cmd_categoryWithIdentifier = Selector(("categoryWithIdentifier:")) | |
| let method_categoryWithIdentifier = class_getClassMethod(EMFEmojiCategory, cmd_categoryWithIdentifier)! | |
| let imp_categoryWithIdentifier = method_getImplementation(method_categoryWithIdentifier) | |
| let func_categoryWithIdentifier = unsafeBitCast(imp_categoryWithIdentifier, to: (@convention(c) (AnyClass, Selector, AnyObject) -> AnyObject).self) | |
| let category = func_categoryWithIdentifier(EMFEmojiCategory, cmd_categoryWithIdentifier, identifierPtr.pointee) | |
| return category | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment