BIN
.assets/Xcode-emoji.mp4
Normal file
BIN
.assets/Xcode-emoji.mp4
Normal file
Binary file not shown.
BIN
.assets/emoji-picker-mac.png
Normal file
BIN
.assets/emoji-picker-mac.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 387 KiB |
BIN
.assets/popover-emoji-picker.png
Normal file
BIN
.assets/popover-emoji-picker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 755 KiB |
@@ -5,9 +5,15 @@ This Swift package allows you to show a view with all available emoji on the OS,
|
|||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
| Emoji list | Emoji search | Emoji settings |
|
| Emoji list | Emoji search | Emoji settings |
|
||||||
|---|---|---|
|
|--------------------------------------------------|----------------------------------------------------------|----------------------------------------------------------|
|
||||||
|  |  |  |
|
|  |  |  |
|
||||||
|
|
||||||
|
## Macos Screenshots
|
||||||
|
| Emoji list | Emoji search |
|
||||||
|
|--------------------------------------------------|----------------------------------------------------------|
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- SwiftUI (iOS >= 15.0)
|
- SwiftUI (iOS >= 15.0)
|
||||||
|
|||||||
94
Sources/EmojiPicker/EmojiCategoryPicker.swift
Normal file
94
Sources/EmojiPicker/EmojiCategoryPicker.swift
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
//
|
||||||
|
// Created by tddworks on 2024/8/1.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import EmojiKit
|
||||||
|
|
||||||
|
public struct EmojiCategoryPicker: View {
|
||||||
|
|
||||||
|
@State var currentCategory: EmojiCategory.Name = EmojiCategory.Name.flags
|
||||||
|
|
||||||
|
private var sections: [EmojiCategory.Name]
|
||||||
|
|
||||||
|
private var selectionHandler: (EmojiCategory.Name) -> Void
|
||||||
|
|
||||||
|
public init(sections: [EmojiCategory.Name], selectionHandler: @escaping (EmojiCategory.Name) -> Void) {
|
||||||
|
self.sections = sections
|
||||||
|
self.selectionHandler = selectionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
SegmentedControl(selection: $currentCategory, dataSource: sections, images: sections.map {
|
||||||
|
NSImage(systemSymbolName: $0.imageName, accessibilityDescription: nil)
|
||||||
|
}
|
||||||
|
.compactMap {
|
||||||
|
$0
|
||||||
|
}, selectionHandler: selectionHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct EmojiCategoryPicker_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let sections = EmojiCategory.Name.orderedCases
|
||||||
|
EmojiCategoryPicker(sections: sections, selectionHandler: { emojiCategoryName in })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Blur: NSViewRepresentable {
|
||||||
|
func makeNSView(context: Context) -> NSVisualEffectView {
|
||||||
|
let view = NSVisualEffectView()
|
||||||
|
view.blendingMode = .withinWindow
|
||||||
|
view.material = .hudWindow
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
|
||||||
|
nsView.blendingMode = .withinWindow
|
||||||
|
nsView.material = .hudWindow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SegmentedControl<T: Hashable>: NSViewRepresentable {
|
||||||
|
@Binding var selection: T
|
||||||
|
|
||||||
|
private let images: [NSImage]
|
||||||
|
private let dataSource: [T]
|
||||||
|
private let selectionHandler: (T) -> Void
|
||||||
|
|
||||||
|
init(selection: Binding<T>, dataSource: [T], images: [NSImage], selectionHandler: @escaping (T) -> Void) {
|
||||||
|
self._selection = selection
|
||||||
|
self.images = images
|
||||||
|
self.dataSource = dataSource
|
||||||
|
self.selectionHandler = selectionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> NSSegmentedControl {
|
||||||
|
let control = NSSegmentedControl(images: images, trackingMode: .selectOne, target: context.coordinator, action: #selector(Coordinator.onChange(_:)))
|
||||||
|
return control
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: NSSegmentedControl, context: Context) {
|
||||||
|
nsView.selectedSegment = dataSource.firstIndex(of: selection) ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> SegmentedControl.Coordinator {
|
||||||
|
Coordinator(parent: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator {
|
||||||
|
let parent: SegmentedControl
|
||||||
|
|
||||||
|
init(parent: SegmentedControl) {
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func onChange(_ control: NSSegmentedControl) {
|
||||||
|
let selection = parent.dataSource[control.selectedSegment]
|
||||||
|
parent.selection = selection
|
||||||
|
parent.selectionHandler(selection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,10 +47,11 @@ public struct EmojiPickerView: View {
|
|||||||
emojiProvider: EmojiProvider = DefaultEmojiProvider(showAllVariations: true)
|
emojiProvider: EmojiProvider = DefaultEmojiProvider(showAllVariations: true)
|
||||||
) {
|
) {
|
||||||
self._selectedEmoji = selectedEmoji
|
self._selectedEmoji = selectedEmoji
|
||||||
self.emojiProvider = emojiProvider
|
self._emojiProvider = State(initialValue: emojiProvider) // Initialize emojiProvider first
|
||||||
|
|
||||||
skinTone1 = emojiProvider.skinTone1
|
// Now you can safely set skinTone1 and skinTone2
|
||||||
skinTone2 = emojiProvider.skinTone2
|
self._skinTone1 = State(initialValue: emojiProvider.skinTone1)
|
||||||
|
self._skinTone2 = State(initialValue: emojiProvider.skinTone2)
|
||||||
}
|
}
|
||||||
|
|
||||||
private let columns = [
|
private let columns = [
|
||||||
@@ -130,6 +131,12 @@ public struct EmojiPickerView: View {
|
|||||||
public var body: some View {
|
public var body: some View {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
VStack {
|
VStack {
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
EmojiSearchView(search: $search)
|
||||||
|
Divider()
|
||||||
|
#endif
|
||||||
|
|
||||||
if isShowingSettings {
|
if isShowingSettings {
|
||||||
VStack {
|
VStack {
|
||||||
settingsView
|
settingsView
|
||||||
@@ -140,7 +147,9 @@ public struct EmojiPickerView: View {
|
|||||||
LazyVGrid(columns: columns, alignment: .leading, pinnedViews: [.sectionHeaders]) {
|
LazyVGrid(columns: columns, alignment: .leading, pinnedViews: [.sectionHeaders]) {
|
||||||
if search.isEmpty {
|
if search.isEmpty {
|
||||||
Section {
|
Section {
|
||||||
ForEach(emojiProvider.frequentlyUsedEmojis.map { $0.sectionedEmoji(EmojiCategory.Name.frequentlyUsed) }, id: \.self) { sectionedEmoji in
|
ForEach(emojiProvider.frequentlyUsedEmojis.map {
|
||||||
|
$0.sectionedEmoji(EmojiCategory.Name.frequentlyUsed)
|
||||||
|
}, id: \.self) { sectionedEmoji in
|
||||||
emojiViewInteractive(emoji: sectionedEmoji.emoji, category: nil)
|
emojiViewInteractive(emoji: sectionedEmoji.emoji, category: nil)
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
@@ -168,30 +177,34 @@ public struct EmojiPickerView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
|
#if os(iOS)
|
||||||
.frame(maxHeight: .infinity)
|
.frame(maxHeight: .infinity)
|
||||||
|
#else
|
||||||
|
.frame(maxHeight: 300)
|
||||||
|
#endif
|
||||||
.autocorrectionDisabled()
|
.autocorrectionDisabled()
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.searchable(text: $search, placement: .navigationBarDrawer(displayMode: .always))
|
.searchable(text: $search, placement: .navigationBarDrawer(displayMode: .always))
|
||||||
.textInputAutocapitalization(.never)
|
.textInputAutocapitalization(.never)
|
||||||
#else
|
|
||||||
.searchable(text: $search, placement: .automatic)
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
VStack {
|
||||||
ForEach(EmojiCategory.Name.orderedCases, id: \.self) { emojiCategoryName in
|
EmojiCategoryPicker(sections: EmojiCategory.Name.orderedCases, selectionHandler: { emojiCategoryName in
|
||||||
Image(systemName: emojiCategoryName.imageName)
|
|
||||||
.font(.system(size: 20))
|
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
.onTapGesture {
|
|
||||||
search = ""
|
search = ""
|
||||||
isShowingSettings = false
|
isShowingSettings = false
|
||||||
proxy.scrollTo(emojiCategoryName, anchor: .topLeading)
|
proxy.scrollTo(emojiCategoryName, anchor: .top)
|
||||||
}
|
}).padding(8)
|
||||||
}
|
}.overlay {
|
||||||
settingsTab
|
RoundedRectangle(cornerRadius: 0)
|
||||||
|
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.background(Color(.windowBackgroundColor))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +323,6 @@ struct SectionedEmoji: Hashable {
|
|||||||
|
|
||||||
struct EmojiPickerView_Previews: PreviewProvider {
|
struct EmojiPickerView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
EmojiPickerView(selectedEmoji: .constant(Emoji(value: "", localizedKeywords: [:])))
|
EmojiPickerView(selectedEmoji: .constant(Emoji(value: "", localizedKeywords: [:])), emojiProvider: DefaultEmojiProvider(showAllVariations: false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
Sources/EmojiPicker/EmojiSearchView.swift
Normal file
23
Sources/EmojiPicker/EmojiSearchView.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// Created by tddworks on 8/11/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EmojiSearchView: View {
|
||||||
|
@Binding var search: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
|
.padding(.leading, 8)
|
||||||
|
TextField("Search emojis",
|
||||||
|
text: $search)
|
||||||
|
.textFieldStyle(PlainTextFieldStyle())
|
||||||
|
.font(Font.system(size: 12))
|
||||||
|
|
||||||
|
}
|
||||||
|
.frame(height: 32)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user