├── .gitignore ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── Example │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ └── ExampleApp.swift └── Package.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── EmojiPalette │ ├── Emoji.swift │ ├── EmojiCategory.swift │ ├── EmojiGroup.swift │ ├── EmojiPaletteModifier.swift │ ├── EmojiPaletteView.swift │ ├── EmojiParser.swift │ ├── EmojiSet.swift │ ├── EmojiSubGroup.swift │ └── Resources │ ├── 14.0-emoji-test.txt │ └── Localizable.xcstrings └── sample.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_Store 3 | 4 | # Xcode 5 | xcuserdata/ 6 | *.xcuserstate 7 | 8 | # Swift Package Manager 9 | Packages.resolved 10 | .swiftpm/ 11 | .build/ 12 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1CAE4C5B2D3FF6E300EBBE15 /* EmojiPalette in Frameworks */ = {isa = PBXBuildFile; productRef = 1CAE4C5A2D3FF6E300EBBE15 /* EmojiPalette */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXFileReference section */ 14 | 1CAE4C3B2D3FF56400EBBE15 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 15 | 1CAE4C522D3FF66000EBBE15 /* EmojiPalette */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = EmojiPalette; path = ..; sourceTree = ""; }; 16 | /* End PBXFileReference section */ 17 | 18 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 19 | 1CAE4C3D2D3FF56400EBBE15 /* Example */ = { 20 | isa = PBXFileSystemSynchronizedRootGroup; 21 | path = Example; 22 | sourceTree = ""; 23 | }; 24 | /* End PBXFileSystemSynchronizedRootGroup section */ 25 | 26 | /* Begin PBXFrameworksBuildPhase section */ 27 | 1CAE4C382D3FF56400EBBE15 /* Frameworks */ = { 28 | isa = PBXFrameworksBuildPhase; 29 | buildActionMask = 2147483647; 30 | files = ( 31 | 1CAE4C5B2D3FF6E300EBBE15 /* EmojiPalette in Frameworks */, 32 | ); 33 | runOnlyForDeploymentPostprocessing = 0; 34 | }; 35 | /* End PBXFrameworksBuildPhase section */ 36 | 37 | /* Begin PBXGroup section */ 38 | 1CAE4C322D3FF56400EBBE15 = { 39 | isa = PBXGroup; 40 | children = ( 41 | 1CAE4C522D3FF66000EBBE15 /* EmojiPalette */, 42 | 1CAE4C3D2D3FF56400EBBE15 /* Example */, 43 | 1CAE4C592D3FF6E300EBBE15 /* Frameworks */, 44 | 1CAE4C3C2D3FF56400EBBE15 /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | 1CAE4C3C2D3FF56400EBBE15 /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 1CAE4C3B2D3FF56400EBBE15 /* Example.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | 1CAE4C592D3FF6E300EBBE15 /* Frameworks */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | ); 60 | name = Frameworks; 61 | sourceTree = ""; 62 | }; 63 | /* End PBXGroup section */ 64 | 65 | /* Begin PBXNativeTarget section */ 66 | 1CAE4C3A2D3FF56400EBBE15 /* Example */ = { 67 | isa = PBXNativeTarget; 68 | buildConfigurationList = 1CAE4C492D3FF56600EBBE15 /* Build configuration list for PBXNativeTarget "Example" */; 69 | buildPhases = ( 70 | 1CAE4C372D3FF56400EBBE15 /* Sources */, 71 | 1CAE4C382D3FF56400EBBE15 /* Frameworks */, 72 | 1CAE4C392D3FF56400EBBE15 /* Resources */, 73 | ); 74 | buildRules = ( 75 | ); 76 | dependencies = ( 77 | ); 78 | fileSystemSynchronizedGroups = ( 79 | 1CAE4C3D2D3FF56400EBBE15 /* Example */, 80 | ); 81 | name = Example; 82 | packageProductDependencies = ( 83 | 1CAE4C5A2D3FF6E300EBBE15 /* EmojiPalette */, 84 | ); 85 | productName = Example; 86 | productReference = 1CAE4C3B2D3FF56400EBBE15 /* Example.app */; 87 | productType = "com.apple.product-type.application"; 88 | }; 89 | /* End PBXNativeTarget section */ 90 | 91 | /* Begin PBXProject section */ 92 | 1CAE4C332D3FF56400EBBE15 /* Project object */ = { 93 | isa = PBXProject; 94 | attributes = { 95 | BuildIndependentTargetsInParallel = 1; 96 | LastSwiftUpdateCheck = 1620; 97 | LastUpgradeCheck = 1620; 98 | TargetAttributes = { 99 | 1CAE4C3A2D3FF56400EBBE15 = { 100 | CreatedOnToolsVersion = 16.2; 101 | }; 102 | }; 103 | }; 104 | buildConfigurationList = 1CAE4C362D3FF56400EBBE15 /* Build configuration list for PBXProject "Example" */; 105 | developmentRegion = en; 106 | hasScannedForEncodings = 0; 107 | knownRegions = ( 108 | en, 109 | Base, 110 | ); 111 | mainGroup = 1CAE4C322D3FF56400EBBE15; 112 | minimizedProjectReferenceProxies = 1; 113 | preferredProjectObjectVersion = 77; 114 | productRefGroup = 1CAE4C3C2D3FF56400EBBE15 /* Products */; 115 | projectDirPath = ""; 116 | projectRoot = ""; 117 | targets = ( 118 | 1CAE4C3A2D3FF56400EBBE15 /* Example */, 119 | ); 120 | }; 121 | /* End PBXProject section */ 122 | 123 | /* Begin PBXResourcesBuildPhase section */ 124 | 1CAE4C392D3FF56400EBBE15 /* Resources */ = { 125 | isa = PBXResourcesBuildPhase; 126 | buildActionMask = 2147483647; 127 | files = ( 128 | ); 129 | runOnlyForDeploymentPostprocessing = 0; 130 | }; 131 | /* End PBXResourcesBuildPhase section */ 132 | 133 | /* Begin PBXSourcesBuildPhase section */ 134 | 1CAE4C372D3FF56400EBBE15 /* Sources */ = { 135 | isa = PBXSourcesBuildPhase; 136 | buildActionMask = 2147483647; 137 | files = ( 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | /* End PBXSourcesBuildPhase section */ 142 | 143 | /* Begin XCBuildConfiguration section */ 144 | 1CAE4C472D3FF56600EBBE15 /* Debug */ = { 145 | isa = XCBuildConfiguration; 146 | buildSettings = { 147 | ALWAYS_SEARCH_USER_PATHS = NO; 148 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 149 | CLANG_ANALYZER_NONNULL = YES; 150 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 151 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 152 | CLANG_ENABLE_MODULES = YES; 153 | CLANG_ENABLE_OBJC_ARC = YES; 154 | CLANG_ENABLE_OBJC_WEAK = YES; 155 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 156 | CLANG_WARN_BOOL_CONVERSION = YES; 157 | CLANG_WARN_COMMA = YES; 158 | CLANG_WARN_CONSTANT_CONVERSION = YES; 159 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 160 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 161 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 162 | CLANG_WARN_EMPTY_BODY = YES; 163 | CLANG_WARN_ENUM_CONVERSION = YES; 164 | CLANG_WARN_INFINITE_RECURSION = YES; 165 | CLANG_WARN_INT_CONVERSION = YES; 166 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 167 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 168 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 169 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 170 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 171 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 172 | CLANG_WARN_STRICT_PROTOTYPES = YES; 173 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 174 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 175 | CLANG_WARN_UNREACHABLE_CODE = YES; 176 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 177 | COPY_PHASE_STRIP = NO; 178 | DEBUG_INFORMATION_FORMAT = dwarf; 179 | ENABLE_STRICT_OBJC_MSGSEND = YES; 180 | ENABLE_TESTABILITY = YES; 181 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 182 | GCC_C_LANGUAGE_STANDARD = gnu17; 183 | GCC_DYNAMIC_NO_PIC = NO; 184 | GCC_NO_COMMON_BLOCKS = YES; 185 | GCC_OPTIMIZATION_LEVEL = 0; 186 | GCC_PREPROCESSOR_DEFINITIONS = ( 187 | "DEBUG=1", 188 | "$(inherited)", 189 | ); 190 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 191 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 192 | GCC_WARN_UNDECLARED_SELECTOR = YES; 193 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 194 | GCC_WARN_UNUSED_FUNCTION = YES; 195 | GCC_WARN_UNUSED_VARIABLE = YES; 196 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 197 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 198 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 199 | MTL_FAST_MATH = YES; 200 | ONLY_ACTIVE_ARCH = YES; 201 | SDKROOT = iphoneos; 202 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 203 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 204 | }; 205 | name = Debug; 206 | }; 207 | 1CAE4C482D3FF56600EBBE15 /* Release */ = { 208 | isa = XCBuildConfiguration; 209 | buildSettings = { 210 | ALWAYS_SEARCH_USER_PATHS = NO; 211 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 212 | CLANG_ANALYZER_NONNULL = YES; 213 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 214 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 215 | CLANG_ENABLE_MODULES = YES; 216 | CLANG_ENABLE_OBJC_ARC = YES; 217 | CLANG_ENABLE_OBJC_WEAK = YES; 218 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 219 | CLANG_WARN_BOOL_CONVERSION = YES; 220 | CLANG_WARN_COMMA = YES; 221 | CLANG_WARN_CONSTANT_CONVERSION = YES; 222 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 223 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 224 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 225 | CLANG_WARN_EMPTY_BODY = YES; 226 | CLANG_WARN_ENUM_CONVERSION = YES; 227 | CLANG_WARN_INFINITE_RECURSION = YES; 228 | CLANG_WARN_INT_CONVERSION = YES; 229 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 230 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 231 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 232 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 233 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 234 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 235 | CLANG_WARN_STRICT_PROTOTYPES = YES; 236 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 237 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 238 | CLANG_WARN_UNREACHABLE_CODE = YES; 239 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 240 | COPY_PHASE_STRIP = NO; 241 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 242 | ENABLE_NS_ASSERTIONS = NO; 243 | ENABLE_STRICT_OBJC_MSGSEND = YES; 244 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 245 | GCC_C_LANGUAGE_STANDARD = gnu17; 246 | GCC_NO_COMMON_BLOCKS = YES; 247 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 248 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 249 | GCC_WARN_UNDECLARED_SELECTOR = YES; 250 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 251 | GCC_WARN_UNUSED_FUNCTION = YES; 252 | GCC_WARN_UNUSED_VARIABLE = YES; 253 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 254 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 255 | MTL_ENABLE_DEBUG_INFO = NO; 256 | MTL_FAST_MATH = YES; 257 | SDKROOT = iphoneos; 258 | SWIFT_COMPILATION_MODE = wholemodule; 259 | VALIDATE_PRODUCT = YES; 260 | }; 261 | name = Release; 262 | }; 263 | 1CAE4C4A2D3FF56600EBBE15 /* Debug */ = { 264 | isa = XCBuildConfiguration; 265 | buildSettings = { 266 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 267 | CODE_SIGN_STYLE = Automatic; 268 | CURRENT_PROJECT_VERSION = 1; 269 | DEVELOPMENT_TEAM = VJ5N2X84K8; 270 | ENABLE_PREVIEWS = YES; 271 | GENERATE_INFOPLIST_FILE = YES; 272 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 273 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 274 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 275 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 276 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 277 | LD_RUNPATH_SEARCH_PATHS = ( 278 | "$(inherited)", 279 | "@executable_path/Frameworks", 280 | ); 281 | MARKETING_VERSION = 1.0; 282 | PRODUCT_BUNDLE_IDENTIFIER = com.kyome.Example; 283 | PRODUCT_NAME = "$(TARGET_NAME)"; 284 | SWIFT_EMIT_LOC_STRINGS = YES; 285 | SWIFT_VERSION = 6.0; 286 | TARGETED_DEVICE_FAMILY = "1,2"; 287 | }; 288 | name = Debug; 289 | }; 290 | 1CAE4C4B2D3FF56600EBBE15 /* Release */ = { 291 | isa = XCBuildConfiguration; 292 | buildSettings = { 293 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 294 | CODE_SIGN_STYLE = Automatic; 295 | CURRENT_PROJECT_VERSION = 1; 296 | DEVELOPMENT_TEAM = VJ5N2X84K8; 297 | ENABLE_PREVIEWS = YES; 298 | GENERATE_INFOPLIST_FILE = YES; 299 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 300 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 301 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 302 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 303 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 304 | LD_RUNPATH_SEARCH_PATHS = ( 305 | "$(inherited)", 306 | "@executable_path/Frameworks", 307 | ); 308 | MARKETING_VERSION = 1.0; 309 | PRODUCT_BUNDLE_IDENTIFIER = com.kyome.Example; 310 | PRODUCT_NAME = "$(TARGET_NAME)"; 311 | SWIFT_EMIT_LOC_STRINGS = YES; 312 | SWIFT_VERSION = 6.0; 313 | TARGETED_DEVICE_FAMILY = "1,2"; 314 | }; 315 | name = Release; 316 | }; 317 | /* End XCBuildConfiguration section */ 318 | 319 | /* Begin XCConfigurationList section */ 320 | 1CAE4C362D3FF56400EBBE15 /* Build configuration list for PBXProject "Example" */ = { 321 | isa = XCConfigurationList; 322 | buildConfigurations = ( 323 | 1CAE4C472D3FF56600EBBE15 /* Debug */, 324 | 1CAE4C482D3FF56600EBBE15 /* Release */, 325 | ); 326 | defaultConfigurationIsVisible = 0; 327 | defaultConfigurationName = Release; 328 | }; 329 | 1CAE4C492D3FF56600EBBE15 /* Build configuration list for PBXNativeTarget "Example" */ = { 330 | isa = XCConfigurationList; 331 | buildConfigurations = ( 332 | 1CAE4C4A2D3FF56600EBBE15 /* Debug */, 333 | 1CAE4C4B2D3FF56600EBBE15 /* Release */, 334 | ); 335 | defaultConfigurationIsVisible = 0; 336 | defaultConfigurationName = Release; 337 | }; 338 | /* End XCConfigurationList section */ 339 | 340 | /* Begin XCSwiftPackageProductDependency section */ 341 | 1CAE4C5A2D3FF6E300EBBE15 /* EmojiPalette */ = { 342 | isa = XCSwiftPackageProductDependency; 343 | productName = EmojiPalette; 344 | }; 345 | /* End XCSwiftPackageProductDependency section */ 346 | }; 347 | rootObject = 1CAE4C332D3FF56400EBBE15 /* Project object */; 348 | } 349 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Example 4 | // 5 | // Created by Takuto Nakamura on 2025/01/22. 6 | // 7 | 8 | import SwiftUI 9 | import EmojiPalette 10 | 11 | struct ContentView: View { 12 | @State var showPopover = false 13 | @State var emoji = "" 14 | 15 | var body: some View { 16 | VStack { 17 | Button { 18 | showPopover = true 19 | } label: { 20 | Text(emoji) 21 | .font(.largeTitle) 22 | } 23 | .buttonStyle(.bordered) 24 | .emojiPalette( 25 | selectedEmoji: $emoji, 26 | isPresented: $showPopover 27 | ) 28 | } 29 | .padding() 30 | .onAppear { 31 | emoji = EmojiParser.shared.randomEmoji()?.character ?? "" 32 | } 33 | } 34 | } 35 | 36 | #Preview { 37 | ContentView() 38 | } 39 | -------------------------------------------------------------------------------- /Example/Example/ExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleApp.swift 3 | // Example 4 | // 5 | // Created by Takuto Nakamura on 2025/01/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct ExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "", 7 | products: [], 8 | targets: [] 9 | ) 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Takuto NAKAMURA (Kyome) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "EmojiPalette", 7 | defaultLocalization: "en", 8 | platforms: [ 9 | .iOS("16.4"), 10 | ], 11 | products: [ 12 | .library( 13 | name: "EmojiPalette", 14 | targets: ["EmojiPalette"] 15 | ), 16 | ], 17 | targets: [ 18 | .target( 19 | name: "EmojiPalette", 20 | resources: [.process("Resources")], 21 | swiftSettings: [.enableUpcomingFeature("ExistentialAny")] 22 | ), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EmojiPalette 2 | 3 | Emoji Picker for iOS using SwiftUI. 4 | 5 | 6 | 7 | ## Requirements 8 | 9 | - Development with Xcode 16.2+ 10 | - Written in Swift 6.0 11 | - Compatible with iOS 16.4+ 12 | 13 | ## Usage 14 | 15 | ```swift 16 | import SwiftUI 17 | import EmojiPalette 18 | 19 | struct ContentView: View { 20 | @State var showPopover = false 21 | @State var emoji = "" 22 | 23 | var body: some View { 24 | VStack { 25 | Button { 26 | showPopover = true 27 | } label: { 28 | Text(emoji) 29 | .font(.largeTitle) 30 | } 31 | .emojiPalette( 32 | selectedEmoji: $emoji, 33 | isPresented: $showPopover 34 | ) 35 | } 36 | .padding() 37 | .onAppear { 38 | emoji = EmojiParser.shared.randomEmoji()?.character ?? "" 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | ## Localization 45 | 46 | - English (en) 47 | - Japanese (ja) 48 | -------------------------------------------------------------------------------- /Sources/EmojiPalette/Emoji.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Emoji.swift 3 | 4 | 5 | Created by Takuto Nakamura on 2023/09/11. 6 | 7 | */ 8 | 9 | public struct Emoji: Sendable, Identifiable { 10 | public var id: String 11 | public var character: String 12 | } 13 | -------------------------------------------------------------------------------- /Sources/EmojiPalette/EmojiCategory.swift: -------------------------------------------------------------------------------- 1 | /* 2 | EmojiCategory.swift 3 | 4 | 5 | Created by Takuto Nakamura on 2023/09/11. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | public enum EmojiCategory: String, Sendable, Identifiable, CaseIterable { 12 | case smileysAndPeople 13 | case animalsAndNature 14 | case foodAndDrink 15 | case activity 16 | case travelAndPlaces 17 | case objects 18 | case symbols 19 | case flags 20 | 21 | public var id: String { rawValue } 22 | 23 | public var label: LocalizedStringKey { .init(rawValue) } 24 | 25 | public var imageName: String { 26 | switch self { 27 | case .smileysAndPeople: "face.smiling" 28 | case .animalsAndNature: "teddybear" 29 | case .foodAndDrink: "fork.knife" 30 | case .activity: "basketball" 31 | case .travelAndPlaces: "car" 32 | case .objects: "lightbulb" 33 | case .symbols: "music.note" 34 | case .flags: "flag" 35 | } 36 | } 37 | 38 | init?(groupName: String) { 39 | switch groupName { 40 | case "Smileys & Emotion": self = .smileysAndPeople 41 | case "People & Body": self = .smileysAndPeople 42 | case "Animals & Nature": self = .animalsAndNature 43 | case "Food & Drink": self = .foodAndDrink 44 | case "Travel & Places": self = .travelAndPlaces 45 | case "Activities": self = .activity 46 | case "Objects": self = .objects 47 | case "Symbols": self = .symbols 48 | case "Flags": self = .flags 49 | default: return nil 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/EmojiPalette/EmojiGroup.swift: -------------------------------------------------------------------------------- 1 | /* 2 | EmojiGroup.swift 3 | 4 | 5 | Created by Takuto Nakamura on 2023/09/12. 6 | 7 | */ 8 | 9 | struct EmojiGroup { 10 | var name: String 11 | var subgroups: [EmojiSubGroup] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/EmojiPalette/EmojiPaletteModifier.swift: -------------------------------------------------------------------------------- 1 | /* 2 | EmojiPaletteModifier.swift 3 | 4 | 5 | Created by Takuto Nakamura on 2023/09/11. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | public struct EmojiPaletteModifier: ViewModifier { 12 | @Binding var selectedEmoji: String 13 | @Binding var isPresented: Bool 14 | var attachmentAnchor: PopoverAttachmentAnchor 15 | var arrowEdge: Edge? 16 | 17 | public init( 18 | selectedEmoji: Binding, 19 | isPresented: Binding, 20 | attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), 21 | arrowEdge: Edge? = nil 22 | ) { 23 | _selectedEmoji = selectedEmoji 24 | _isPresented = isPresented 25 | self.attachmentAnchor = attachmentAnchor 26 | self.arrowEdge = arrowEdge 27 | } 28 | 29 | public func body(content: Content) -> some View { 30 | content.popover( 31 | isPresented: $isPresented, 32 | attachmentAnchor: attachmentAnchor, 33 | arrowEdge: arrowEdge 34 | ) { 35 | EmojiPaletteView(selectedEmoji: $selectedEmoji) 36 | .clipShape(Rectangle()) 37 | .presentationCompactAdaptation(.popover) 38 | } 39 | } 40 | } 41 | 42 | extension View { 43 | public func emojiPalette( 44 | selectedEmoji: Binding, 45 | isPresented: Binding, 46 | attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), 47 | arrowEdge: Edge? = nil 48 | ) -> some View { 49 | modifier(EmojiPaletteModifier( 50 | selectedEmoji: selectedEmoji, 51 | isPresented: isPresented, 52 | attachmentAnchor: attachmentAnchor, 53 | arrowEdge: arrowEdge 54 | )) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/EmojiPalette/EmojiPaletteView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | EmojiPaletteView.swift 3 | 4 | 5 | Created by Takuto Nakamura on 2023/09/11. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | public struct EmojiPaletteView: View { 12 | @Binding var selectedEmoji: String 13 | @State var emojiSets: [EmojiSet] 14 | @State var selection = EmojiCategory.smileysAndPeople 15 | private var columns = [GridItem](repeating: .init(.flexible(), spacing: 8), count: 6) 16 | 17 | public init(selectedEmoji: Binding) { 18 | _selectedEmoji = selectedEmoji 19 | emojiSets = EmojiParser.shared.emojiSets 20 | } 21 | 22 | public var body: some View { 23 | VStack(spacing: 0) { 24 | if let emojiSet = emojiSets.first(where: { $0.category == selection }) { 25 | List { 26 | Section { 27 | LazyVGrid(columns: columns, spacing: 8) { 28 | ForEach(emojiSet.emojis) { emoji in 29 | Button { 30 | selectedEmoji = emoji.character 31 | } label: { 32 | Text(emoji.character) 33 | .font(.system(size: 26)) 34 | } 35 | .buttonStyle(.borderless) 36 | .frame(width: 32, height: 32) 37 | } 38 | } 39 | } header: { 40 | Text(emojiSet.category.label, bundle: .module) 41 | } 42 | .textCase(.none) 43 | } 44 | .listStyle(.plain) 45 | .id(selection) 46 | } 47 | Divider() 48 | HStack(spacing: 8) { 49 | ForEach(EmojiCategory.allCases) { emojiCategory in 50 | Image(systemName: emojiCategory.imageName) 51 | .font(.system(size: 18)) 52 | .frame(width: 24, height: 24) 53 | .foregroundColor(selection == emojiCategory ? Color.accentColor : .secondary) 54 | .onTapGesture { 55 | selection = emojiCategory 56 | } 57 | } 58 | } 59 | .padding(8) 60 | } 61 | .frame(width: 264, height: 280) // width = (2 * 16) + (6 * 32) + (5 * 8) = 264 62 | } 63 | } 64 | 65 | #Preview { 66 | EmojiPaletteView(selectedEmoji: .constant("")) 67 | } 68 | -------------------------------------------------------------------------------- /Sources/EmojiPalette/EmojiParser.swift: -------------------------------------------------------------------------------- 1 | /* 2 | EmojiParser.swift 3 | 4 | 5 | Created by Takuto Nakamura on 2023/09/10. 6 | 7 | */ 8 | 9 | import Foundation 10 | import os 11 | 12 | public final class EmojiParser: Sendable { 13 | public static let shared = EmojiParser() 14 | 15 | private let protectedEmojiSets = OSAllocatedUnfairLock<[EmojiSet]>(initialState: []) 16 | 17 | public var emojiSets: [EmojiSet] { 18 | protectedEmojiSets.withLock(\.self) 19 | } 20 | 21 | private init() { 22 | let groups = loadEmojiGroup() 23 | let emojiSets = groups.reduce(into: [EmojiSet]()) { partialResult, group in 24 | guard let category = EmojiCategory(groupName: group.name) else { 25 | return 26 | } 27 | let emojis = group.subgroups.flatMap(\.emojis) 28 | if partialResult.last?.category == category { 29 | partialResult[partialResult.count - 1].emojis += emojis 30 | } else { 31 | partialResult.append(EmojiSet(category: category, emojis: emojis)) 32 | } 33 | } 34 | protectedEmojiSets.withLock { $0 = emojiSets } 35 | } 36 | 37 | private func loadEmojiGroup() -> [EmojiGroup] { 38 | guard let path = Bundle.module.path(forResource: "14.0-emoji-test", ofType: "txt"), 39 | let data = try? Data(contentsOf: URL(fileURLWithPath: path)), 40 | let text = String(data: data, encoding: .utf8) else { 41 | fatalError("Could not get data from 14.0-emoji-test.txt") 42 | } 43 | var groups = [EmojiGroup]() 44 | var subgroups = [EmojiSubGroup]() 45 | var emojis = [Emoji]() 46 | text.enumerateLines { line, stop in 47 | if line.contains("# group:"), 48 | let group = line.components(separatedBy: "# group:").last?.trimmingCharacters(in: .whitespaces) { 49 | subgroups = [] 50 | groups.append(EmojiGroup(name: group, subgroups: [])) 51 | } 52 | if line.contains("# subgroup:"), 53 | let subGroup = line.components(separatedBy: "# subgroup:").last?.trimmingCharacters(in: .whitespaces) { 54 | emojis = [] 55 | subgroups.append(EmojiSubGroup(name: subGroup, emojis: [])) 56 | groups[groups.count - 1].subgroups = subgroups 57 | } 58 | if line.contains(";") && !line.contains("Format:") { 59 | let separatedBySemicolon = line.split(separator: ";") 60 | if let separatedByHash = separatedBySemicolon.last?.split(separator: "#"), 61 | let status = separatedByHash.first?.trimmingCharacters(in: .whitespaces), 62 | let afterHash = separatedByHash.last?.trimmingCharacters(in: .whitespaces), 63 | let emoji = afterHash.components(separatedBy: .whitespaces).first { 64 | guard status != "unqualified" && status != "minimally-qualified" else { 65 | return 66 | } 67 | guard !afterHash.contains(":") || !afterHash.contains("skin tone") else { 68 | return 69 | } 70 | var array = afterHash.components(separatedBy: " ") 71 | array.removeFirst() 72 | array.removeFirst() 73 | let id = array.map { $0.replacingOccurrences(of: ":", with: "") }.joined(separator: "-") 74 | emojis.append(Emoji(id: id, character: emoji)) 75 | subgroups[subgroups.count - 1].emojis = emojis 76 | groups[groups.count - 1].subgroups = subgroups 77 | } 78 | } 79 | } 80 | return groups 81 | } 82 | 83 | public func randomEmoji(categories: [EmojiCategory] = EmojiCategory.allCases) -> Emoji? { 84 | protectedEmojiSets.withLock(\.self) 85 | .filter { categories.contains($0.category) } 86 | .randomElement()? 87 | .emojis 88 | .randomElement() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/EmojiPalette/EmojiSet.swift: -------------------------------------------------------------------------------- 1 | /* 2 | EmojiSet.swift 3 | 4 | 5 | Created by Takuto Nakamura on 2023/09/11. 6 | 7 | */ 8 | 9 | public struct EmojiSet: Sendable, Identifiable { 10 | public var category: EmojiCategory 11 | public var emojis: [Emoji] 12 | public var id: String { category.rawValue } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/EmojiPalette/EmojiSubGroup.swift: -------------------------------------------------------------------------------- 1 | /* 2 | EmojiSubGroup.swift 3 | 4 | 5 | Created by Takuto Nakamura on 2023/09/12. 6 | 7 | */ 8 | 9 | struct EmojiSubGroup { 10 | var name: String 11 | var emojis: [Emoji] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/EmojiPalette/Resources/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "activity" : { 5 | "extractionState" : "manual", 6 | "localizations" : { 7 | "en" : { 8 | "stringUnit" : { 9 | "state" : "translated", 10 | "value" : "Activity" 11 | } 12 | }, 13 | "ja" : { 14 | "stringUnit" : { 15 | "state" : "translated", 16 | "value" : "活動" 17 | } 18 | } 19 | } 20 | }, 21 | "animalsAndNature" : { 22 | "extractionState" : "manual", 23 | "localizations" : { 24 | "en" : { 25 | "stringUnit" : { 26 | "state" : "translated", 27 | "value" : "Animals & Nature" 28 | } 29 | }, 30 | "ja" : { 31 | "stringUnit" : { 32 | "state" : "translated", 33 | "value" : "動物と自然" 34 | } 35 | } 36 | } 37 | }, 38 | "flags" : { 39 | "extractionState" : "manual", 40 | "localizations" : { 41 | "en" : { 42 | "stringUnit" : { 43 | "state" : "translated", 44 | "value" : "Flags" 45 | } 46 | }, 47 | "ja" : { 48 | "stringUnit" : { 49 | "state" : "translated", 50 | "value" : "旗" 51 | } 52 | } 53 | } 54 | }, 55 | "foodAndDrink" : { 56 | "extractionState" : "manual", 57 | "localizations" : { 58 | "en" : { 59 | "stringUnit" : { 60 | "state" : "translated", 61 | "value" : "Food & Drink" 62 | } 63 | }, 64 | "ja" : { 65 | "stringUnit" : { 66 | "state" : "translated", 67 | "value" : "食べ物と飲み物" 68 | } 69 | } 70 | } 71 | }, 72 | "objects" : { 73 | "extractionState" : "manual", 74 | "localizations" : { 75 | "en" : { 76 | "stringUnit" : { 77 | "state" : "translated", 78 | "value" : "Objects" 79 | } 80 | }, 81 | "ja" : { 82 | "stringUnit" : { 83 | "state" : "translated", 84 | "value" : "物" 85 | } 86 | } 87 | } 88 | }, 89 | "smileysAndPeople" : { 90 | "comment" : "Localizable.strings\n \n\n Created by Takuto Nakamura on 2023/09/11.", 91 | "extractionState" : "manual", 92 | "localizations" : { 93 | "en" : { 94 | "stringUnit" : { 95 | "state" : "translated", 96 | "value" : "Smileys & People" 97 | } 98 | }, 99 | "ja" : { 100 | "stringUnit" : { 101 | "state" : "translated", 102 | "value" : "スマイリーと人々" 103 | } 104 | } 105 | } 106 | }, 107 | "symbols" : { 108 | "extractionState" : "manual", 109 | "localizations" : { 110 | "en" : { 111 | "stringUnit" : { 112 | "state" : "translated", 113 | "value" : "Symbols" 114 | } 115 | }, 116 | "ja" : { 117 | "stringUnit" : { 118 | "state" : "translated", 119 | "value" : "記号" 120 | } 121 | } 122 | } 123 | }, 124 | "travelAndPlaces" : { 125 | "extractionState" : "manual", 126 | "localizations" : { 127 | "en" : { 128 | "stringUnit" : { 129 | "state" : "translated", 130 | "value" : "Travel & Places" 131 | } 132 | }, 133 | "ja" : { 134 | "stringUnit" : { 135 | "state" : "translated", 136 | "value" : "旅行と場所" 137 | } 138 | } 139 | } 140 | } 141 | }, 142 | "version" : "1.0" 143 | } -------------------------------------------------------------------------------- /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/EmojiPalette/f6444756f0759e3ffd351c25554d96f62827990c/sample.png --------------------------------------------------------------------------------