Add support for search

This commit is contained in:
2024-06-12 23:45:09 -04:00
parent af3212e70e
commit 2dafcc1287
6 changed files with 184 additions and 46 deletions

View File

@@ -5,8 +5,7 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/EmojiKit", "location" : "https://github.com/tyiu/EmojiKit",
"state" : { "state" : {
"branch" : "emoji-keywords", "revision" : "719d405244ea9ef462867c16e3d3254b7386b71f"
"revision" : "99744e7e8d004e1e7ebaa0e38af184ae1b6c8296"
} }
}, },
{ {
@@ -17,6 +16,15 @@
"revision" : "ee97538f5b81ae89698fd95938896dec5217b148", "revision" : "ee97538f5b81ae89698fd95938896dec5217b148",
"version" : "1.1.1" "version" : "1.1.1"
} }
},
{
"identity" : "swift-trie",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/swift-trie",
"state" : {
"revision" : "0bb65eec3d570e8a0f6bd5c6a72f10641b97c71e",
"version" : "0.1.1"
}
} }
], ],
"version" : 2 "version" : 2

View File

@@ -15,14 +15,18 @@ let package = Package(
dependencies: [ dependencies: [
// Dependencies declare other packages that this package depends on. // Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"), // .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/tyiu/EmojiKit", branch: "emoji-keywords") // .package(url: "https://github.com/tyiu/EmojiKit", branch: "emoji-keywords")
.package(url: "https://github.com/tyiu/EmojiKit", revision: "719d405244ea9ef462867c16e3d3254b7386b71f"),
.package(url: "https://github.com/tyiu/swift-trie", .upToNextMajor(from: "0.1.1"))
], ],
targets: [ targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on. // Targets can depend on other targets in this package, and on products in packages this package depends on.
.target( .target(
name: "EmojiPicker", name: "EmojiPicker",
dependencies: ["EmojiKit"]), dependencies: [
.product(name: "EmojiKit", package: "EmojiKit"),
.product(name: "SwiftTrie", package: "swift-trie")]),
.testTarget( .testTarget(
name: "EmojiPickerTests", name: "EmojiPickerTests",
dependencies: ["EmojiPicker"]), dependencies: ["EmojiPicker"]),

View File

@@ -19,14 +19,15 @@ It is a SwiftUI library that allows you to get a list of all the emojis present
## Dependencies ## Dependencies
- SwiftUI (iOS >= 15.0) - SwiftUI (iOS >= 15.0)
- [Smile](https://github.com/onmyway133/Smile) (2.1.0) - [EmojiKit](https://github.com/tyiu/EmojiKit) (`719d405244ea9ef462867c16e3d3254b7386b71f`)
- [SwiftTrie](https://github.com/tyiu/swift-trie) (1.1.0)
## How install it? ## How install it?
Nowaday we only support Swift Package Manager. You can use build-in UI tool for XCode with this search words: `EmojiPicker` or you can add it directly with this following command : Nowaday we only support Swift Package Manager. You can use build-in UI tool for XCode with this search words: `EmojiPicker` or you can add it directly with this following command :
```swift ```swift
.package(url: "https://github.com/Kelvas09/EmojiPicker.git", from: "1.0.0") .package(url: "https://github.com/tyiu/EmojiPicker.git", from: "2.0.0")
``` ```
## How use it? ## How use it?
@@ -154,3 +155,7 @@ NavigationView {
## Samples ## Samples
You can access to sample project on folder `EmojiPickerSample` You can access to sample project on folder `EmojiPickerSample`
## Acknowledgements
This Swift package was forked from [Kelvas09/EmojiPicker](https://github.com/Kelvas09/EmojiPicker).

View File

@@ -7,17 +7,58 @@
import Foundation import Foundation
import EmojiKit import EmojiKit
import SwiftTrie
public final class DefaultEmojiProvider: EmojiProvider { public final class DefaultEmojiProvider: EmojiProvider {
public init() { } private let emojiCategoriesCache = EmojiManager.getAvailableEmojis()
private let trie = Trie<Emoji>()
public func getAppleEmojiCategories() -> [EmojiKit.AppleEmojiCategory] { // Unicode ranges for skin tone modifiers
return EmojiManager.getAvailableEmojis() private let skinToneRanges: [ClosedRange<UInt32>] = [
0x1F3FB...0x1F3FF // Skin tone modifiers
]
public init() {
emojiCategories.forEach { category in
category.emojis.forEach { emoji in
let emojiValue = emoji.value.value
// Insert the emoji itself as a searchable string in the trie.
let _ = trie.insert(key: emojiValue, value: emoji.value, options: [.includeNonPrefixedMatches])
let emojiWithoutSkinTone = removeSkinTone(emojiValue)
if emojiWithoutSkinTone != emojiValue {
let _ = trie.insert(key: emojiWithoutSkinTone, value: emoji.value, options: [.includeNonPrefixedMatches])
}
// Insert all the localized keywords for the emoji in the trie.
emoji.value.localizedKeywords.forEach { locale in
locale.value.forEach { keyword in
let _ = trie.insert(key: keyword, value: emoji.value, options: [.includeNonPrefixedMatches, .includeCaseInsensitiveMatches, .includeDiacriticsInsensitiveMatches])
}
}
}
}
} }
public func getAllEmojis() -> [EmojiKit.Emoji] { // Function to check if a scalar is a skin tone modifier
return EmojiManager.getAvailableEmojis().flatMap { $0.emojis.values } private func isSkinToneModifier(scalar: Unicode.Scalar) -> Bool {
return skinToneRanges.contains { $0.contains(scalar.value) }
}
private func removeSkinTone(_ string: String) -> String {
let filteredScalars = string.unicodeScalars.filter { !isSkinToneModifier(scalar: $0) }
return String(String.UnicodeScalarView(filteredScalars))
}
public var emojiCategories: [AppleEmojiCategory] {
emojiCategoriesCache
}
public func find(query: String) -> [Emoji] {
let queryWithoutSkinTone = removeSkinTone(query)
return trie.find(key: queryWithoutSkinTone.localizedLowercase)
} }
} }

View File

@@ -7,6 +7,7 @@
import SwiftUI import SwiftUI
import EmojiKit import EmojiKit
import SwiftTrie
public struct EmojiPickerView: View { public struct EmojiPickerView: View {
@@ -16,6 +17,9 @@ public struct EmojiPickerView: View {
@Binding @Binding
public var selectedEmoji: Emoji? public var selectedEmoji: Emoji?
@State
var selectedCategoryName: EmojiCategory.Name = .smileysAndPeople
@State @State
private var search: String = "" private var search: String = ""
@@ -23,55 +27,131 @@ public struct EmojiPickerView: View {
private var searchEnabled: Bool private var searchEnabled: Bool
private let emojiCategories: [AppleEmojiCategory] private let emojiCategories: [AppleEmojiCategory]
private let emojiProvider: EmojiProvider
public init(selectedEmoji: Binding<Emoji?>, searchEnabled: Bool = false, selectedColor: Color = .blue, emojiProvider: EmojiProvider = DefaultEmojiProvider(), emojiCategories: [AppleEmojiCategory] = EmojiManager.getAvailableEmojis()) { public init(selectedEmoji: Binding<Emoji?>, searchEnabled: Bool = false, selectedColor: Color = Color.accentColor, emojiProvider: EmojiProvider = DefaultEmojiProvider()) {
self._selectedEmoji = selectedEmoji self._selectedEmoji = selectedEmoji
self.selectedColor = selectedColor self.selectedColor = selectedColor
self.searchEnabled = searchEnabled self.searchEnabled = searchEnabled
self.emojiCategories = emojiProvider.getAppleEmojiCategories() self.emojiProvider = emojiProvider
self.emojiCategories = emojiProvider.emojiCategories
} }
let columns = [ let columns = [
GridItem(.adaptive(minimum: 36)) GridItem(.adaptive(minimum: 36))
] ]
public var body: some View { private var searchResults: [Emoji] {
ScrollView { if search.isEmpty {
LazyVGrid(columns: columns, alignment: .leading) { return []
ForEach(emojiCategories, id: \.self) { category in } else {
Section { return emojiProvider.find(query: search)
ForEach(category.emojis.values, id: \.self) { emoji in
RoundedRectangle(cornerRadius: 16)
.fill((selectedEmoji == emoji ? selectedColor : Color.clear).opacity(0.4))
.frame(width: 36, height: 36)
.overlay {
Text(emoji.value)
.font(.largeTitle)
}
.onTapGesture {
selectedEmoji = emoji
dismiss()
}
}
} header: {
Text(category.name.localizedName)
.foregroundStyle(.gray)
.padding(.vertical, 8)
}
.frame(alignment: .leading)
}
}
.padding(.horizontal)
} }
.frame(maxHeight: .infinity)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
} }
public var body: some View {
ScrollViewReader { proxy in
VStack {
ScrollView {
LazyVGrid(columns: columns, alignment: .leading) {
if !searchEnabled || search.isEmpty {
ForEach(emojiCategories, id: \.self) { category in
Section {
ForEach(category.emojis.values, id: \.self) { emoji in
RoundedRectangle(cornerRadius: 16)
.fill((selectedEmoji == emoji ? selectedColor : Color.clear).opacity(0.4))
.frame(width: 36, height: 36)
.overlay {
Text(emoji.value)
.font(.largeTitle)
}
.onTapGesture {
selectedEmoji = emoji
dismiss()
}
.onAppear {
self.selectedCategoryName = category.name
}
}
} header: {
Text(category.name.localizedName)
.foregroundStyle(.gray)
.padding(.vertical, 8)
}
.frame(alignment: .leading)
.id(category.name)
.onAppear {
self.selectedCategoryName = category.name
}
}
} else {
ForEach(searchResults, id: \.self) { emoji in
RoundedRectangle(cornerRadius: 16)
.fill((selectedEmoji == emoji ? selectedColor : Color.clear).opacity(0.4))
.frame(width: 36, height: 36)
.overlay {
Text(emoji.value)
.font(.largeTitle)
}
.onTapGesture {
selectedEmoji = emoji
dismiss()
}
}
}
}
.padding(.horizontal)
}
.frame(maxHeight: .infinity)
.searchable(text: $search, placement: .navigationBarDrawer(displayMode: .always))
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
if search.isEmpty {
HStack(spacing: 8) {
ForEach(EmojiCategory.Name.orderedCases, id: \.self) { emojiCategoryName in
Image(systemName: emojiCategoryName.imageName)
.font(.system(size: 18))
.frame(width: 24, height: 24)
.foregroundColor(selectedCategoryName == emojiCategoryName ? Color.accentColor : .secondary)
.onTapGesture {
selectedCategoryName = emojiCategoryName
proxy.scrollTo(emojiCategoryName, anchor: .top)
}
}
}
}
}
}
}
}
extension AppleEmojiCategory.Name {
var imageName: String {
switch self {
case .smileysAndPeople:
return "face.smiling"
case .animalsAndNature:
return "teddybear"
case .foodAndDrink:
return "fork.knife"
case .activity:
return "basketball"
case .travelAndPlaces:
return "car"
case .objects:
return "lightbulb"
case .symbols:
return "music.note"
case .flags:
return "flag"
}
}
} }
struct EmojiPickerView_Previews: PreviewProvider { struct EmojiPickerView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
EmojiPickerView(selectedEmoji: .constant(Emoji(value: "", keywords: []))) EmojiPickerView(selectedEmoji: .constant(Emoji(value: "", localizedKeywords: [:])), searchEnabled: true)
} }
} }

View File

@@ -9,6 +9,6 @@ import Foundation
import EmojiKit import EmojiKit
public protocol EmojiProvider { public protocol EmojiProvider {
func getAppleEmojiCategories() -> [AppleEmojiCategory] var emojiCategories: [AppleEmojiCategory] { get }
func getAllEmojis() -> [Emoji] func find(query: String) -> [Emoji]
} }