Add frequently used emojis and multiple skin tone support

This commit is contained in:
2024-06-17 23:17:50 -04:00
parent 9f40464d27
commit 0c28b4a1a6
14 changed files with 486 additions and 127 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 KiB

After

Width:  |  Height:  |  Size: 88 KiB

BIN
.assets/EmojiPicker-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -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"
}
}
],

View File

@@ -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)

View File

@@ -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"
}
}
],

View File

@@ -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")
]
)
]
)

View File

@@ -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`

View File

@@ -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<Emoji>()
private let emojiCategoriesCache: [EmojiCategory]
private let trie: Trie<Emoji>
private let allVariations: [String: [Emoji]]
// Unicode ranges for skin tone modifiers
private let skinToneRanges: [ClosedRange<UInt32>] = [
0x1F3FB...0x1F3FF // Skin tone modifiers
]
private let skinToneRange: ClosedRange<UInt32> = 0x1F3FB...0x1F3FF
public init(showAllVariations: Bool) {
let trie = Trie<Emoji>()
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)
}
}

View File

@@ -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<Emoji?>, selectedColor: Color = Color.accentColor, emojiProvider: EmojiProvider = DefaultEmojiProvider()) {
private let demoSkinToneEmojis: [String] = [
"👍",
"🧍",
"🧑",
"🤝",
"🧑‍🤝‍🧑",
"💏",
"💑"
]
public init(
selectedEmoji: Binding<Emoji?>,
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: [:])))

View File

@@ -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?
}

View File

@@ -0,0 +1,4 @@
"firstSkinTone" = "First Skin Tone";
"reset" = "Reset";
"secondSkinTone" = "Second Skin Tone";
"skinToneHeader" = "Skin Tone";

View File

@@ -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
}
}
}

View File

@@ -1,6 +0,0 @@
import XCTest
@testable import EmojiPicker
final class EmojiPickerTests: XCTestCase {
}