From 2dafcc1287a45f98cd498366af1a510d5604cefe Mon Sep 17 00:00:00 2001 From: Terry Yiu <963907+tyiu@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:45:09 -0400 Subject: [PATCH] Add support for search --- Package.resolved | 12 +- Package.swift | 8 +- README.md | 9 +- .../EmojiPicker/DefaultEmojiProvider.swift | 51 +++++- Sources/EmojiPicker/EmojiPickerView.swift | 146 ++++++++++++++---- Sources/EmojiPicker/EmojiProvider.swift | 4 +- 6 files changed, 184 insertions(+), 46 deletions(-) diff --git a/Package.resolved b/Package.resolved index cf01517..2db2c8e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tyiu/EmojiKit", "state" : { - "branch" : "emoji-keywords", - "revision" : "99744e7e8d004e1e7ebaa0e38af184ae1b6c8296" + "revision" : "719d405244ea9ef462867c16e3d3254b7386b71f" } }, { @@ -17,6 +16,15 @@ "revision" : "ee97538f5b81ae89698fd95938896dec5217b148", "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 diff --git a/Package.swift b/Package.swift index c64acd8..7d6c89a 100644 --- a/Package.swift +++ b/Package.swift @@ -15,14 +15,18 @@ let package = Package( dependencies: [ // Dependencies declare other packages that this package depends on. // .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 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. .target( name: "EmojiPicker", - dependencies: ["EmojiKit"]), + dependencies: [ + .product(name: "EmojiKit", package: "EmojiKit"), + .product(name: "SwiftTrie", package: "swift-trie")]), .testTarget( name: "EmojiPickerTests", dependencies: ["EmojiPicker"]), diff --git a/README.md b/README.md index 1e09af2..4cfdc99 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,15 @@ It is a SwiftUI library that allows you to get a list of all the emojis present ## Dependencies - 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? 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 -.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? @@ -154,3 +155,7 @@ NavigationView { ## Samples You can access to sample project on folder `EmojiPickerSample` + +## Acknowledgements + +This Swift package was forked from [Kelvas09/EmojiPicker](https://github.com/Kelvas09/EmojiPicker). diff --git a/Sources/EmojiPicker/DefaultEmojiProvider.swift b/Sources/EmojiPicker/DefaultEmojiProvider.swift index 4dc04ad..a22c2d1 100644 --- a/Sources/EmojiPicker/DefaultEmojiProvider.swift +++ b/Sources/EmojiPicker/DefaultEmojiProvider.swift @@ -7,17 +7,58 @@ import Foundation import EmojiKit +import SwiftTrie public final class DefaultEmojiProvider: EmojiProvider { - public init() { } + private let emojiCategoriesCache = EmojiManager.getAvailableEmojis() + private let trie = Trie() - public func getAppleEmojiCategories() -> [EmojiKit.AppleEmojiCategory] { - return EmojiManager.getAvailableEmojis() + // Unicode ranges for skin tone modifiers + private let skinToneRanges: [ClosedRange] = [ + 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] { - return EmojiManager.getAvailableEmojis().flatMap { $0.emojis.values } + // Function to check if a scalar is a skin tone modifier + 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) } } diff --git a/Sources/EmojiPicker/EmojiPickerView.swift b/Sources/EmojiPicker/EmojiPickerView.swift index b9cc613..d4c5c4c 100644 --- a/Sources/EmojiPicker/EmojiPickerView.swift +++ b/Sources/EmojiPicker/EmojiPickerView.swift @@ -7,6 +7,7 @@ import SwiftUI import EmojiKit +import SwiftTrie public struct EmojiPickerView: View { @@ -16,6 +17,9 @@ public struct EmojiPickerView: View { @Binding public var selectedEmoji: Emoji? + @State + var selectedCategoryName: EmojiCategory.Name = .smileysAndPeople + @State private var search: String = "" @@ -23,55 +27,131 @@ public struct EmojiPickerView: View { private var searchEnabled: Bool private let emojiCategories: [AppleEmojiCategory] + private let emojiProvider: EmojiProvider - public init(selectedEmoji: Binding, searchEnabled: Bool = false, selectedColor: Color = .blue, emojiProvider: EmojiProvider = DefaultEmojiProvider(), emojiCategories: [AppleEmojiCategory] = EmojiManager.getAvailableEmojis()) { + public init(selectedEmoji: Binding, searchEnabled: Bool = false, selectedColor: Color = Color.accentColor, emojiProvider: EmojiProvider = DefaultEmojiProvider()) { self._selectedEmoji = selectedEmoji self.selectedColor = selectedColor self.searchEnabled = searchEnabled - self.emojiCategories = emojiProvider.getAppleEmojiCategories() + self.emojiProvider = emojiProvider + self.emojiCategories = emojiProvider.emojiCategories } let columns = [ GridItem(.adaptive(minimum: 36)) ] - public var body: some View { - ScrollView { - LazyVGrid(columns: columns, alignment: .leading) { - 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() - } - } - } header: { - Text(category.name.localizedName) - .foregroundStyle(.gray) - .padding(.vertical, 8) - } - .frame(alignment: .leading) - } - } - .padding(.horizontal) + private var searchResults: [Emoji] { + if search.isEmpty { + return [] + } else { + return emojiProvider.find(query: search) } - .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 { static var previews: some View { - EmojiPickerView(selectedEmoji: .constant(Emoji(value: "", keywords: []))) + EmojiPickerView(selectedEmoji: .constant(Emoji(value: "", localizedKeywords: [:])), searchEnabled: true) } } diff --git a/Sources/EmojiPicker/EmojiProvider.swift b/Sources/EmojiPicker/EmojiProvider.swift index 53c65c7..4993229 100644 --- a/Sources/EmojiPicker/EmojiProvider.swift +++ b/Sources/EmojiPicker/EmojiProvider.swift @@ -9,6 +9,6 @@ import Foundation import EmojiKit public protocol EmojiProvider { - func getAppleEmojiCategories() -> [AppleEmojiCategory] - func getAllEmojis() -> [Emoji] + var emojiCategories: [AppleEmojiCategory] { get } + func find(query: String) -> [Emoji] }