4 Commits

6 changed files with 121 additions and 40 deletions

View File

@@ -1,7 +1,13 @@
MIT License MIT License
Copyright (c) 2024 Terry Yiu
Fork of https://github.com/niklasamslgruber/EmojiKit
Copyright (c) 2023 Niklas Amslgruber Copyright (c) 2023 Niklas Amslgruber
Apple emoji category name localizations forked from https://github.com/izyumkin/MCEmojiPicker
Copyright (c) 2022 Ivan Izyumkin
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights

View File

@@ -14,8 +14,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git", "location" : "https://github.com/apple/swift-collections.git",
"state" : { "state" : {
"revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", "revision" : "ee97538f5b81ae89698fd95938896dec5217b148",
"version" : "1.1.0" "version" : "1.1.1"
} }
}, },
{ {

View File

@@ -14,7 +14,7 @@ let package = Package(
) )
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.1.0")), .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.1.1")),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"),
], ],

View File

@@ -19,7 +19,7 @@ In most cases it is enough to just use the `EmojiKit` for your app. The `EmojiSo
##### SwiftPM ##### SwiftPM
``` ```
https://github.com/niklasamslgruber/EmojiKit https://github.com/tyiu/EmojiKit
``` ```
### Usage ### Usage
@@ -89,3 +89,8 @@ Currently only to Unicode releases are supported (Version 14 and 15). If you wan
In that case make sure that you added the `emojis_vX.json` file to your Xcode project. The file name must match the version you're trying to fetch emojis for, e.g. for version 12 the file name must be `emojis_v12.json`. Additionally make sure that your JSON file is added under `Build Phase - Copy Bundle Resources` for each target where you want to use the `EmojiManager`. In that case make sure that you added the `emojis_vX.json` file to your Xcode project. The file name must match the version you're trying to fetch emojis for, e.g. for version 12 the file name must be `emojis_v12.json`. Additionally make sure that your JSON file is added under `Build Phase - Copy Bundle Resources` for each target where you want to use the `EmojiManager`.
## Acknowledgements
This EmojiKit Swift package was forked from [niklasamslgruber/EmojiKit](https://github.com/niklasamslgruber/EmojiKit).
Some of the localization files for Apple emoji category names were copied from [izyumkin/MCEmojiPicker](https://github.com/izyumkin/MCEmojiPicker).

View File

@@ -16,18 +16,20 @@ public class AppleEmojiCategory: Codable, Hashable {
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(name) hasher.combine(name)
hasher.combine(emojis) hasher.combine(emojis)
hasher.combine(variations)
} }
public enum Name: String, CaseIterable, Codable { public enum Name: String, CaseIterable, Codable {
case flags = "flags" case frequentlyUsed = "frequentlyUsed"
case activity = "activity" case smileysAndPeople = "smileysAndPeople"
case objects = "objects"
case travelAndPlaces = "travelAndPlaces"
case symbols = "symbols"
case animalsAndNature = "animalsAndNature" case animalsAndNature = "animalsAndNature"
case foodAndDrink = "foodAndDrink" case foodAndDrink = "foodAndDrink"
case smileysAndPeople = "smileysAndPeople" case activity = "activity"
case travelAndPlaces = "travelAndPlaces"
case objects = "objects"
case symbols = "symbols"
case flags = "flags"
public static var orderedCases: [Name] { public static var orderedCases: [Name] {
return allCases.sorted(by: { $0.order < $1.order }) return allCases.sorted(by: { $0.order < $1.order })
@@ -35,22 +37,24 @@ public class AppleEmojiCategory: Codable, Hashable {
public var order: Int { public var order: Int {
switch self { switch self {
case .flags: case .frequentlyUsed:
return 8 return 0
case .activity: case .smileysAndPeople:
return 4 return 1
case .objects:
return 6
case .travelAndPlaces:
return 5
case .symbols:
return 7
case .animalsAndNature: case .animalsAndNature:
return 2 return 2
case .foodAndDrink: case .foodAndDrink:
return 3 return 3
case .smileysAndPeople: case .activity:
return 1 return 4
case .travelAndPlaces:
return 5
case .objects:
return 6
case .symbols:
return 7
case .flags:
return 8
} }
} }
@@ -66,10 +70,12 @@ public class AppleEmojiCategory: Codable, Hashable {
public let name: Name public let name: Name
public var emojis: OrderedDictionary<String, Emoji> public var emojis: OrderedDictionary<String, Emoji>
public var variations: [String: [Emoji]]
public init(name: Name, emojis: OrderedDictionary<String, Emoji>) { public init(name: Name, emojis: OrderedDictionary<String, Emoji>, variations: [String: [Emoji]]) {
self.name = name self.name = name
self.emojis = emojis self.emojis = emojis
self.variations = variations
} }
} }

View File

@@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import OrderedCollections
public typealias EmojiCategory = AppleEmojiCategory public typealias EmojiCategory = AppleEmojiCategory
@@ -47,6 +48,32 @@ public enum EmojiManager {
} }
} }
// When skin tone modifiers are stripped from some emojis,
// they don't have the same Unicode scalar values as the neutral version.
// We need to maintain a manual mapping so that the lists of variations are accurate.
private static let emojiSpecialMapping: [[UInt32]: UInt32] = [
[0x1FAF1, 0x200D, 0x1FAF2]: 0x1F91D, // 🤝 handshake
[0x1F469, 0x200D, 0x1F91D, 0x200D, 0x1F469]: 0x1F46D, // 👭 women holding hands
[0x1F469, 0x200D, 0x1F91D, 0x200D, 0x1F468]: 0x1F46B, // 👫 woman and man holding hands
[0x1F468, 0x200D, 0x1F91D, 0x200D, 0x1F468]: 0x1F46C, // 👬 men holding hands
[0x1F9D1, 0x200D, 0x2764, 0x200D, 0x1F48B, 0x200D, 0x1F9D1]: 0x1F48F, // 💏 kiss: person, person
[0x1F9D1, 0x200D, 0x2764, 0x200D, 0x1F9D1]: 0x1F491, // 💑 couple with heart: person, person
]
private static func uint32ToEmoji(_ value: UInt32) -> String? {
// Create a Unicode scalar from the UInt32 value
guard let scalar = UnicodeScalar(value) else {
print("Invalid Unicode scalar value")
return nil
}
// Create a Character from the Unicode scalar
let character = Character(scalar)
// Convert the Character to a String and return it
return String(character)
}
/// Returns all emojis for a specific version /// Returns all emojis for a specific version
/// - Parameters: /// - Parameters:
/// - version: The specific version you want to fetch (default: the highest supported version for a device's iOS version) /// - version: The specific version you want to fetch (default: the highest supported version for a device's iOS version)
@@ -59,25 +86,43 @@ public enum EmojiManager {
var filteredEmojis: [UnicodeEmojiCategory] = [] var filteredEmojis: [UnicodeEmojiCategory] = []
var appleCategories: [AppleEmojiCategory] = [] var appleCategories: [AppleEmojiCategory] = []
for category in result { for category in result {
let supportedEmojis = category.emojis.filter({ var variations = [String: [Emoji]]()
showAllVariations ? true : isNeutralEmoji(for: $0.key) var supportedEmojis = OrderedDictionary<String, Emoji>()
}) category.emojis.forEach {
if isNeutralEmoji(for: $0.key) {
supportedEmojis[$0.key] = $0.value
} else if showAllVariations {
let unqualifiedNeutralEmoji = unqualifiedNeutralEmoji(for: $0.key)
if let variationsForEmoji = variations[unqualifiedNeutralEmoji] {
variations[unqualifiedNeutralEmoji] = variationsForEmoji + [$0.value]
} else {
variations[unqualifiedNeutralEmoji] = [$0.value]
}
}
}
let unicodeCategory = UnicodeEmojiCategory(name: category.name, emojis: supportedEmojis) let unicodeCategory = UnicodeEmojiCategory(name: category.name, emojis: supportedEmojis)
filteredEmojis.append(unicodeCategory) filteredEmojis.append(unicodeCategory)
if shouldMergeCategory(category), let index = appleCategories.firstIndex(where: { $0.name == .smileysAndPeople }) { if shouldMergeCategory(category), let index = appleCategories.firstIndex(where: { $0.name == .smileysAndPeople }) {
if category.name == .smileysAndEmotions { if category.name == .smileysAndEmotions {
let oldValues = appleCategories[index].emojis let oldEmojis = appleCategories[index].emojis
appleCategories[index].emojis = supportedEmojis appleCategories[index].emojis = supportedEmojis
appleCategories[index].emojis.merge(oldValues) { (current, _) in current } appleCategories[index].emojis.merge(oldEmojis) { (current, _) in current }
let oldVariations = appleCategories[index].variations
appleCategories[index].variations = variations
appleCategories[index].variations.merge(oldVariations) { (current, _) in current }
} else { } else {
appleCategories[index].emojis.merge(supportedEmojis) { (current, _) in current } appleCategories[index].emojis.merge(supportedEmojis) { (current, _) in current }
appleCategories[index].variations.merge(variations) { (current, _) in current }
} }
} else { } else {
guard let appleCategory = unicodeCategory.appleCategory else { guard let appleCategory = unicodeCategory.appleCategory else {
continue continue
} }
appleCategories.append(AppleEmojiCategory(name: appleCategory, emojis: supportedEmojis)) appleCategories.append(AppleEmojiCategory(name: appleCategory, emojis: supportedEmojis, variations: variations))
} }
} }
return appleCategories.sorted(by: { $0.name.order < $1.name.order }) return appleCategories.sorted(by: { $0.name.order < $1.name.order })
@@ -89,19 +134,38 @@ public enum EmojiManager {
return category.name == .smileysAndEmotions || category.name == .peopleAndBody return category.name == .smileysAndEmotions || category.name == .peopleAndBody
} }
private static func isNeutralEmoji(for emoji: String) -> Bool { private static let skinToneRange: ClosedRange<UInt32> = 0x1F3FB...0x1F3FF
let unicodes = getUnicodes(emoji: emoji)
let colors = ["1F3FB", "1F3FC", "1F3FD", "1F3FE", "1F3FF"]
for color in colors where unicodes.contains(color) { public static func isNeutralEmoji(for emojiValue: String) -> Bool {
return false return emojiValue.unicodeScalars.allSatisfy { !skinToneRange.contains($0.value) }
}
return true
} }
private static func getUnicodes(emoji: String) -> [String] { public static func isSkinToneModifier(scalar: Unicode.Scalar) -> Bool {
let unicodeScalars = emoji.unicodeScalars return skinToneRange.contains(scalar.value)
let unicodes = unicodeScalars.map { $0.value } }
return unicodes.map { String($0, radix: 16, uppercase: true) }
public static func neutralEmoji(for emojiValue: String) -> String {
let filteredScalars = emojiValue.unicodeScalars.filter { !isSkinToneModifier(scalar: $0) }
return String(String.UnicodeScalarView(filteredScalars))
}
public static func unqualifiedNeutralEmoji(for emoji: String) -> String {
let variationSelector: Character = "\u{FE0F}"
var unqualifiedEmoji = ""
for scalar in neutralEmoji(for: emoji).unicodeScalars {
let character = Character(scalar)
if character != variationSelector {
unqualifiedEmoji.append(character)
}
}
let unicodeScalars = unqualifiedEmoji.unicodeScalars.map { $0.value }
if let actualUnqualifiedNeutralScalar = emojiSpecialMapping[unicodeScalars],
let actualUnqualifiedNeutralEmoji = uint32ToEmoji(actualUnqualifiedNeutralScalar) {
return String(actualUnqualifiedNeutralEmoji)
}
return unqualifiedEmoji
} }
} }