diff --git a/.assets/EmojiPicker-1.png b/.assets/EmojiPicker-1.png index e4cbbd5..220b099 100644 Binary files a/.assets/EmojiPicker-1.png and b/.assets/EmojiPicker-1.png differ diff --git a/.assets/EmojiPicker-2.png b/.assets/EmojiPicker-2.png index d20f475..d7dff4a 100644 Binary files a/.assets/EmojiPicker-2.png and b/.assets/EmojiPicker-2.png differ diff --git a/.assets/EmojiPicker-3.png b/.assets/EmojiPicker-3.png new file mode 100644 index 0000000..0858418 Binary files /dev/null and b/.assets/EmojiPicker-3.png differ diff --git a/EmojiPickerSample/EmojiPickerSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/EmojiPickerSample/EmojiPickerSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f76dd46..bb40cd3 100644 --- a/EmojiPickerSample/EmojiPickerSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/EmojiPickerSample/EmojiPickerSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tyiu/EmojiKit", "state" : { - "revision" : "719d405244ea9ef462867c16e3d3254b7386b71f", - "version" : "0.1.0" + "revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a", + "version" : "0.1.2" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tyiu/swift-trie", "state" : { - "revision" : "0bb65eec3d570e8a0f6bd5c6a72f10641b97c71e", - "version" : "0.1.1" + "revision" : "4c50bff6c168f74425f70476be62a072980d2da7", + "version" : "0.1.2" } } ], diff --git a/EmojiPickerSample/EmojiPickerSample/ContentView.swift b/EmojiPickerSample/EmojiPickerSample/ContentView.swift index 87e067c..66210b2 100644 --- a/EmojiPickerSample/EmojiPickerSample/ContentView.swift +++ b/EmojiPickerSample/EmojiPickerSample/ContentView.swift @@ -36,7 +36,7 @@ struct ContentView: View { .padding() .sheet(isPresented: $displayEmojiPicker) { NavigationView { - EmojiPickerView(selectedEmoji: $selectedEmoji, selectedColor: .orange) + EmojiPickerView(selectedEmoji: $selectedEmoji) .padding(.top, 32) .navigationTitle("Emojis") .navigationBarTitleDisplayMode(.inline) diff --git a/Package.resolved b/Package.resolved index f76dd46..bb40cd3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tyiu/EmojiKit", "state" : { - "revision" : "719d405244ea9ef462867c16e3d3254b7386b71f", - "version" : "0.1.0" + "revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a", + "version" : "0.1.2" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tyiu/swift-trie", "state" : { - "revision" : "0bb65eec3d570e8a0f6bd5c6a72f10641b97c71e", - "version" : "0.1.1" + "revision" : "4c50bff6c168f74425f70476be62a072980d2da7", + "version" : "0.1.2" } } ], diff --git a/Package.swift b/Package.swift index d6bc5c7..400f0e6 100644 --- a/Package.swift +++ b/Package.swift @@ -5,18 +5,19 @@ import PackageDescription let package = Package( name: "EmojiPicker", - platforms: [.iOS(.v15)], + defaultLocalization: "en", + platforms: [.macOS(.v13), .iOS(.v15)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "EmojiPicker", - targets: ["EmojiPicker"]), + targets: ["EmojiPicker"]) ], 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", .upToNextMajor(from: "0.1.0")), - .package(url: "https://github.com/tyiu/swift-trie", .upToNextMajor(from: "0.1.1")) + .package(url: "https://github.com/tyiu/EmojiKit", .upToNextMajor(from: "0.1.2")), + .package(url: "https://github.com/tyiu/swift-trie", .upToNextMajor(from: "0.1.2")) ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -25,9 +26,11 @@ let package = Package( name: "EmojiPicker", dependencies: [ .product(name: "EmojiKit", package: "EmojiKit"), - .product(name: "SwiftTrie", package: "swift-trie")]), - .testTarget( - name: "EmojiPickerTests", - dependencies: ["EmojiPicker"]), + .product(name: "SwiftTrie", package: "swift-trie") + ], + resources: [ + .process("Resources") + ] + ) ] ) diff --git a/README.md b/README.md index fe40ecd..ae8619a 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,15 @@ This Swift package allows you to show a view with all available emoji on the OS, ## Screenshots -|Emoji list|Emoji search| -|---|---| -|![Emoji list](./.assets/EmojiPicker-1.png)|![Emoji search](./.assets/EmojiPicker-2.png)| +|Emoji list|Emoji search|Emoji settings| +|---|---|---| +|![Emoji list](./.assets/EmojiPicker-1.png)|![Emoji search](./.assets/EmojiPicker-2.png)|![Emoji settings](./.assets/EmojiPicker-3.png)| ## Dependencies - SwiftUI (iOS >= 15.0) -- [EmojiKit](https://github.com/tyiu/EmojiKit) (0.1.0) -- [SwiftTrie](https://github.com/tyiu/swift-trie) (0.1.1) +- [EmojiKit](https://github.com/tyiu/EmojiKit) (0.1.2) +- [SwiftTrie](https://github.com/tyiu/swift-trie) (0.1.2) ## Installation @@ -39,7 +39,7 @@ let package = Package( // ... dependencies: [ // ... - .package(url: "https://github.com/tyiu/EmojiPicker.git", .upToNextMajor(from: "0.1.0")) + .package(url: "https://github.com/tyiu/EmojiPicker.git", .upToNextMajor(from: "0.1.1")) ], targets: [ .target( @@ -96,7 +96,7 @@ struct ContentView: View { .padding() .sheet(isPresented: $displayEmojiPicker) { NavigationView { - EmojiPickerView(selectedEmoji: $selectedEmoji, selectedColor: .orange) + EmojiPickerView(selectedEmoji: $selectedEmoji) .padding(.top, 32) .navigationTitle("Emojis") .navigationBarTitleDisplayMode(.inline) @@ -107,14 +107,6 @@ struct ContentView: View { } ``` -### Select color - -When a user selects an emoji, it is highlighted. By default the selection color is `blue` but you can change this value when creating the view: - -```swift -EmojiPickerView(selectedEmoji: $selectedEmoji, selectedColor: .orange) -``` - ## Samples You can access to sample project on folder `EmojiPickerSample` diff --git a/Sources/EmojiPicker/DefaultEmojiProvider.swift b/Sources/EmojiPicker/DefaultEmojiProvider.swift index a22c2d1..9413b12 100644 --- a/Sources/EmojiPicker/DefaultEmojiProvider.swift +++ b/Sources/EmojiPicker/DefaultEmojiProvider.swift @@ -10,55 +10,211 @@ import EmojiKit import SwiftTrie public final class DefaultEmojiProvider: EmojiProvider { + private let showAllVariations: Bool - private let emojiCategoriesCache = EmojiManager.getAvailableEmojis() - private let trie = Trie() + private let emojiCategoriesCache: [EmojiCategory] + private let trie: Trie + + private let allVariations: [String: [Emoji]] // Unicode ranges for skin tone modifiers - private let skinToneRanges: [ClosedRange] = [ - 0x1F3FB...0x1F3FF // Skin tone modifiers - ] + private let skinToneRange: ClosedRange = 0x1F3FB...0x1F3FF + + public init(showAllVariations: Bool) { + let trie = Trie() + let emojiCategories = EmojiManager.getAvailableEmojis(showAllVariations: showAllVariations) + var allVariations = [String: [Emoji]]() - 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]) - } + _ = trie.insert(key: emojiValue, 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]) + _ = trie.insert( + key: keyword, + value: emoji.value, + options: [ + .includeNonPrefixedMatches, + .includeCaseInsensitiveMatches, + .includeDiacriticsInsensitiveMatches + ] + ) } } } + + allVariations.merge(category.variations, uniquingKeysWith: { (current, _) in current }) } + + self.showAllVariations = showAllVariations + self.trie = trie + self.emojiCategoriesCache = emojiCategories + self.allVariations = allVariations + } + + public var isShowingAllVariations: Bool { + showAllVariations } // 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)) + return skinToneRange.contains(scalar.value) } public var emojiCategories: [AppleEmojiCategory] { emojiCategoriesCache } + public var variations: [String: [Emoji]] { + allVariations + } + public func find(query: String) -> [Emoji] { - let queryWithoutSkinTone = removeSkinTone(query) + let queryWithoutSkinTone = EmojiManager.neutralEmoji(for: query) return trie.find(key: queryWithoutSkinTone.localizedLowercase) } + public func variation(for emojiValue: String, skinTone1: SkinTone, skinTone2: SkinTone) -> Emoji? { + let unqualifiedNeutralEmoji = EmojiManager.unqualifiedNeutralEmoji(for: emojiValue) + guard let variationsForEmoji = variations[unqualifiedNeutralEmoji] else { + return nil + } + + let skinTone1Scalar = skinTone1.unicodeScalarValue + let skinTone2Scalar = skinTone2.unicodeScalarValue + + // Sort variations by number of skin tone modifiers so that we can find the variation + // that matches closest to our skin tone search parameters. + // Some emojis can have 0-2 skin tone modifier variations. + // Some emojis can have 0-1 skin tone modifier variations. + let sortedVariationsForEmoji = variationsForEmoji.sorted { + let count1 = $0.value.unicodeScalars.filter { scalar in isSkinToneModifier(scalar: scalar) }.count + let count2 = $1.value.unicodeScalars.filter { scalar in isSkinToneModifier(scalar: scalar) }.count + + return count1 > count2 + } + + for variation in sortedVariationsForEmoji { + let skinToneScalars = variation.value.unicodeScalars + .filter { isSkinToneModifier(scalar: $0) }.map { $0.value } + + switch skinToneScalars.count { + case 0: + continue + case 1: + if skinTone1Scalar == skinToneScalars[0] { + return variation + } + default: + if skinTone1Scalar == skinToneScalars[0] && skinTone2Scalar == skinToneScalars[1] { + return variation + } + } + } + + return nil + } + + public var frequentlyUsedEmojis: [Emoji] { + return Array( + emojiCategoriesCache + .flatMap { $0.emojis.values } + .filter { $0.usageCount > 0 } + .sorted(by: { lhs, rhs in + let (aUsage, bUsage) = (lhs.usage, rhs.usage) + guard aUsage.count != bUsage.count else { + // Break ties with most recent usage + return lhs.lastUsage > rhs.lastUsage + } + return aUsage.count > bUsage.count + }) + .prefix(30) + ) + } + + public func removeFrequentlyUsedEmojis() { + emojiCategoriesCache + .flatMap { $0.emojis.values } + .filter { $0.usageCount > 0 } + .forEach { + UserDefaults.standard.removeObject(forKey: StorageKeys.usageTimestamps($0).key) + } + } + + public var skinTone1: SkinTone { + get { + guard let skinToneScalar = UserDefaults.standard.object(forKey: StorageKeys.skinTone1.key) as? UInt32 else { + return .neutral + } + return SkinTone.allCases.first(where: { + $0.unicodeScalarValue == skinToneScalar + }) ?? .neutral + } + set { + if newValue != .neutral, let unicodeScalarValue = newValue.unicodeScalarValue { + UserDefaults.standard.set(unicodeScalarValue, forKey: StorageKeys.skinTone1.key) + } else { + UserDefaults.standard.removeObject(forKey: StorageKeys.skinTone1.key) + } + } + } + + public var skinTone2: SkinTone { + get { + guard let skinToneScalar = UserDefaults.standard.object(forKey: StorageKeys.skinTone2.key) as? UInt32 else { + return .neutral + } + return SkinTone.allCases.first(where: { $0.unicodeScalarValue == skinToneScalar }) ?? .neutral + } + set { + if newValue != .neutral, let unicodeScalarValue = newValue.unicodeScalarValue { + UserDefaults.standard.set(unicodeScalarValue, forKey: StorageKeys.skinTone2.key) + } else { + UserDefaults.standard.removeObject(forKey: StorageKeys.skinTone2.key) + } + } + } +} + +private enum StorageKeys { + case skinTone1 + case skinTone2 + case usageTimestamps(_ emoji: Emoji) + + var key: String { + switch self { + case .skinTone1: + return "emojipicker-skintone1" + case .skinTone2: + return "emojipicker-skintone2" + case .usageTimestamps(let emoji): + return "emojipicker-usage-timestamps-" + EmojiManager.unqualifiedNeutralEmoji(for: emoji.value) + } + } +} + +extension Emoji { + /// All times when the emoji has been selected. + var usage: [TimeInterval] { + (UserDefaults.standard.array(forKey: StorageKeys.usageTimestamps(self).key) as? [TimeInterval]) ?? [] + } + /// The number of times this emoji has been selected. + var usageCount: Int { + usage.count + } + /// The last time when this emoji has been selected. + var lastUsage: TimeInterval { + usage.first ?? .zero + } + + /// Increments the usage count for this emoji. + func incrementUsageCount() { + let nowTimestamp = Date().timeIntervalSince1970 + UserDefaults.standard.set([nowTimestamp] + usage, forKey: StorageKeys.usageTimestamps(self).key) + } } diff --git a/Sources/EmojiPicker/EmojiPickerView.swift b/Sources/EmojiPicker/EmojiPickerView.swift index edd10f2..2a47ea6 100644 --- a/Sources/EmojiPicker/EmojiPickerView.swift +++ b/Sources/EmojiPicker/EmojiPickerView.swift @@ -12,30 +12,48 @@ import SwiftTrie public struct EmojiPickerView: View { @Environment(\.dismiss) - var dismiss + private var dismiss @Binding - public var selectedEmoji: Emoji? + private var selectedEmoji: Emoji? @State - var selectedCategoryName: EmojiCategory.Name = .smileysAndPeople + private var skinTone1: SkinTone + + @State + private var skinTone2: SkinTone @State private var search: String = "" - private var selectedColor: Color + @State + private var isShowingSettings: Bool = false - private let emojiCategories: [AppleEmojiCategory] - private let emojiProvider: EmojiProvider + @State + private var emojiProvider: EmojiProvider - public init(selectedEmoji: Binding, selectedColor: Color = Color.accentColor, emojiProvider: EmojiProvider = DefaultEmojiProvider()) { + private let demoSkinToneEmojis: [String] = [ + "👍", + "🧍", + "🧑", + "🤝", + "🧑‍🤝‍🧑", + "💏", + "💑" + ] + + public init( + selectedEmoji: Binding, + emojiProvider: EmojiProvider = DefaultEmojiProvider(showAllVariations: true) + ) { self._selectedEmoji = selectedEmoji - self.selectedColor = selectedColor self.emojiProvider = emojiProvider - self.emojiCategories = emojiProvider.emojiCategories + + skinTone1 = emojiProvider.skinTone1 + skinTone2 = emojiProvider.skinTone2 } - let columns = [ + private let columns = [ GridItem(.adaptive(minimum: 36)) ] @@ -47,87 +65,218 @@ public struct EmojiPickerView: View { } } + private func emojiVariation(_ emoji: Emoji) -> Emoji { + let unqualifiedNeutralEmoji = EmojiManager.unqualifiedNeutralEmoji(for: emoji.value) + if (skinTone1 == .neutral && skinTone2 == .neutral) + || emojiProvider.variations[unqualifiedNeutralEmoji] == nil { + // Show neutral emoji if both skin tones are neutral. + return emoji + } else if let variation = emojiProvider.variation( + for: emoji.value, + skinTone1: skinTone1, + skinTone2: skinTone2 + ) { + // Show skin tone combination if the variation exists. + return variation + } else if skinTone2 == .neutral, let variation = emojiProvider.variation( + for: emoji.value, + skinTone1: skinTone1, + skinTone2: skinTone1 + ) { + // If only the second skin tone is neutral, + // look up only variations where the second skin tone is the same as the first. + return variation + } else { + // If none of the above are found, show the neutral emoji. + return emoji + } + } + + private func emojiView(emoji: Emoji, category: AppleEmojiCategory?) -> some View { + RoundedRectangle(cornerRadius: 16) + .fill(.clear) + .frame(width: 36, height: 36) + .overlay { + Text(emojiVariation(emoji).value) + .font(.largeTitle) + } + } + + private func emojiViewInteractive(emoji: Emoji, category: AppleEmojiCategory?) -> some View { + emojiView(emoji: emoji, category: category) + .onTapGesture { + emoji.incrementUsageCount() + selectedEmoji = emojiVariation(emoji) + dismiss() + } + } + + private func sectionHeaderView(_ categoryName: EmojiCategory.Name) -> some View { + ZStack { +#if os(iOS) + Color(.systemBackground) + .frame(maxWidth: .infinity) // Ensure background spans full width +#else + Color(.windowBackgroundColor) + .frame(maxWidth: .infinity) // Ensure background spans full width +#endif + Text(categoryName.localizedName) + .foregroundStyle(.gray) + .frame(maxWidth: .infinity, alignment: .leading) // Ensure text is aligned + } + .zIndex(1) // Ensure header is on top + } + public var body: some View { ScrollViewReader { proxy in VStack { - ScrollView { - LazyVGrid(columns: columns, alignment: .leading) { - if search.isEmpty { - ForEach(emojiCategories, id: \.self) { category in + if isShowingSettings { + VStack { + settingsView + } + .frame(maxHeight: .infinity) + } else { + ScrollView { + LazyVGrid(columns: columns, alignment: .leading, pinnedViews: [.sectionHeaders]) { + if search.isEmpty { 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 - } + ForEach(emojiProvider.frequentlyUsedEmojis.map { $0.sectionedEmoji(EmojiCategory.Name.frequentlyUsed) }, id: \.self) { sectionedEmoji in + emojiViewInteractive(emoji: sectionedEmoji.emoji, category: nil) } } header: { - Text(category.name.localizedName) - .foregroundStyle(.gray) - .padding(.vertical, 8) + sectionHeaderView(EmojiCategory.Name.frequentlyUsed) } + .id(EmojiCategory.Name.frequentlyUsed) .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) + ForEach(emojiProvider.emojiCategories, id: \.self) { category in + Section { + ForEach(category.emojis.values, id: \.self) { emoji in + emojiViewInteractive(emoji: emoji, category: category) + } + } header: { + sectionHeaderView(category.name) + } + .id(category.name) + .frame(alignment: .leading) } + } else { + ForEach(searchResults, id: \.self) { emoji in + emojiViewInteractive(emoji: emoji, category: nil) + } + } } + .padding(.horizontal) } + .frame(maxHeight: .infinity) + .autocorrectionDisabled() +#if os(iOS) + .searchable(text: $search, placement: .navigationBarDrawer(displayMode: .always)) + .textInputAutocapitalization(.never) +#else + .searchable(text: $search, placement: .automatic) +#endif + } + + HStack(spacing: 8) { + ForEach(EmojiCategory.Name.orderedCases, id: \.self) { emojiCategoryName in + Image(systemName: emojiCategoryName.imageName) + .font(.system(size: 20)) + .frame(width: 24, height: 24) + .onTapGesture { + search = "" + isShowingSettings = false + proxy.scrollTo(emojiCategoryName, anchor: .topLeading) + } + } + settingsTab } } } } + private var settingsView: some View { + Form { + Section { + HStack { + ForEach(demoSkinToneEmojis, id: \.self) { + emojiView(emoji: Emoji(value: $0, localizedKeywords: [:]), category: nil) + .frame(alignment: .center) + } + } + + Picker( + NSLocalizedString("firstSkinTone", + tableName: "EmojiPickerLocalizable", + bundle: .module, + comment: ""), + selection: $skinTone1 + ) { + ForEach(SkinTone.allCases, id: \.self) { skinTone in + Text(skinTone.rawValue) + } + } + .pickerStyle(.segmented) + .onChange(of: skinTone1) { newSkinTone in + emojiProvider.skinTone1 = newSkinTone + } + + Picker( + NSLocalizedString("secondSkinTone", + tableName: "EmojiPickerLocalizable", + bundle: .module, + comment: ""), + selection: $skinTone2 + ) { + ForEach(SkinTone.allCases, id: \.self) { skinTone in + Text(skinTone.rawValue) + } + } + .pickerStyle(.segmented) + .onChange(of: skinTone2) { newSkinTone in + emojiProvider.skinTone2 = newSkinTone + } + } header: { + Text(NSLocalizedString("skinToneHeader", + tableName: "EmojiPickerLocalizable", + bundle: .module, + comment: "")) + } + + Section { + Button { + emojiProvider.removeFrequentlyUsedEmojis() + } label: { + Text(NSLocalizedString("reset", + tableName: "EmojiPickerLocalizable", + bundle: .module, + comment: "")) + } + } header: { + Text(EmojiCategory.Name.frequentlyUsed.localizedName) + } + } + } + + private var settingsTab: some View { + Image(systemName: "gear") + .font(.system(size: 20)) + .frame(width: 24, height: 24) + .foregroundColor(isShowingSettings + ? Color.accentColor : .secondary) + .onTapGesture { + search = "" + isShowingSettings = true + } + } + } extension AppleEmojiCategory.Name { var imageName: String { switch self { + case .frequentlyUsed: + return "clock" case .smileysAndPeople: return "face.smiling" case .animalsAndNature: @@ -148,6 +297,17 @@ extension AppleEmojiCategory.Name { } } +extension Emoji { + func sectionedEmoji(_ categoryName: EmojiCategory.Name) -> SectionedEmoji { + SectionedEmoji(emoji: self, categoryName: categoryName) + } +} + +struct SectionedEmoji: Hashable { + let emoji: Emoji + let categoryName: EmojiCategory.Name +} + struct EmojiPickerView_Previews: PreviewProvider { static var previews: some View { EmojiPickerView(selectedEmoji: .constant(Emoji(value: "", localizedKeywords: [:]))) diff --git a/Sources/EmojiPicker/EmojiProvider.swift b/Sources/EmojiPicker/EmojiProvider.swift index 4993229..092539c 100644 --- a/Sources/EmojiPicker/EmojiProvider.swift +++ b/Sources/EmojiPicker/EmojiProvider.swift @@ -9,6 +9,13 @@ import Foundation import EmojiKit public protocol EmojiProvider { + var isShowingAllVariations: Bool { get } var emojiCategories: [AppleEmojiCategory] { get } + var variations: [String: [Emoji]] { get } + var skinTone1: SkinTone { get set } + var skinTone2: SkinTone { get set } + var frequentlyUsedEmojis: [Emoji] { get } + func removeFrequentlyUsedEmojis() func find(query: String) -> [Emoji] + func variation(for emojiValue: String, skinTone1: SkinTone, skinTone2: SkinTone) -> Emoji? } diff --git a/Sources/EmojiPicker/Resources/en.lproj/EmojiPickerLocalizable.strings b/Sources/EmojiPicker/Resources/en.lproj/EmojiPickerLocalizable.strings new file mode 100644 index 0000000..5932f4e --- /dev/null +++ b/Sources/EmojiPicker/Resources/en.lproj/EmojiPickerLocalizable.strings @@ -0,0 +1,4 @@ +"firstSkinTone" = "First Skin Tone"; +"reset" = "Reset"; +"secondSkinTone" = "Second Skin Tone"; +"skinToneHeader" = "Skin Tone"; diff --git a/Sources/EmojiPicker/SkinTone.swift b/Sources/EmojiPicker/SkinTone.swift new file mode 100644 index 0000000..f0f46d9 --- /dev/null +++ b/Sources/EmojiPicker/SkinTone.swift @@ -0,0 +1,43 @@ +// +// SkinTone.swift +// +// +// Created by Terry Yiu on 6/15/24. +// + +import Foundation + +public enum SkinTone: String, CaseIterable { + case neutral = "🟨" + case light = "🏻" + case mediumLight = "🏼" + case medium = "🏽" + case mediumDark = "🏾" + case dark = "🏿" + + static public var allCases: [SkinTone] = [ + .neutral, + .light, + .mediumLight, + .medium, + .mediumDark, + .dark + ] + + public var unicodeScalarValue: UInt32? { + switch self { + case .neutral: + nil + case .light: + 0x1F3FB + case .mediumLight: + 0x1F3FC + case .medium: + 0x1F3FD + case .mediumDark: + 0x1F3FE + case .dark: + 0x1F3FF + } + } +} diff --git a/Tests/EmojiPickerTests/EmojiPickerTests.swift b/Tests/EmojiPickerTests/EmojiPickerTests.swift deleted file mode 100644 index 4c64aaf..0000000 --- a/Tests/EmojiPickerTests/EmojiPickerTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -import XCTest -@testable import EmojiPicker - -final class EmojiPickerTests: XCTestCase { - -}