├── .gitignore ├── Icon ├── LICENSE ├── README.md ├── SwiftUI AdaptiveImageGlyph.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── SwiftUI AdaptiveImageGlyph ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Composer.swift ├── ContentView.swift ├── FontStyles.swift ├── Persistence.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── RichTextEditor.swift ├── SwiftUI_AdaptiveImageGlyph.xcdatamodeld │ ├── .xccurrentversion │ └── SwiftUI_AdaptiveImageGlyph.xcdatamodel │ │ └── contents └── SwiftUI_AdaptiveImageGlyphApp.swift └── assets ├── banner.png └── genmoji.icns /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,xcode,swift 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,xcode,swift 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Swift ### 38 | # Xcode 39 | # 40 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 41 | 42 | ## User settings 43 | xcuserdata/ 44 | 45 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 46 | *.xcscmblueprint 47 | *.xccheckout 48 | 49 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 50 | build/ 51 | DerivedData/ 52 | *.moved-aside 53 | *.pbxuser 54 | !default.pbxuser 55 | *.mode1v3 56 | !default.mode1v3 57 | *.mode2v3 58 | !default.mode2v3 59 | *.perspectivev3 60 | !default.perspectivev3 61 | 62 | ## Obj-C/Swift specific 63 | *.hmap 64 | 65 | ## App packaging 66 | *.ipa 67 | *.dSYM.zip 68 | *.dSYM 69 | 70 | ## Playgrounds 71 | timeline.xctimeline 72 | playground.xcworkspace 73 | 74 | # Swift Package Manager 75 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 76 | # Packages/ 77 | # Package.pins 78 | # Package.resolved 79 | # *.xcodeproj 80 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 81 | # hence it is not needed unless you have added a package configuration file to your project 82 | # .swiftpm 83 | 84 | .build/ 85 | 86 | # CocoaPods 87 | # We recommend against adding the Pods directory to your .gitignore. However 88 | # you should judge for yourself, the pros and cons are mentioned at: 89 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 90 | # Pods/ 91 | # Add this line if you want to avoid checking in source code from the Xcode workspace 92 | # *.xcworkspace 93 | 94 | # Carthage 95 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 96 | # Carthage/Checkouts 97 | 98 | Carthage/Build/ 99 | 100 | # Accio dependency management 101 | Dependencies/ 102 | .accio/ 103 | 104 | # fastlane 105 | # It is recommended to not store the screenshots in the git repo. 106 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 107 | # For more information about the recommended setup visit: 108 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 109 | 110 | fastlane/report.xml 111 | fastlane/Preview.html 112 | fastlane/screenshots/**/*.png 113 | fastlane/test_output 114 | 115 | # Code Injection 116 | # After new code Injection tools there's a generated folder /iOSInjectionProject 117 | # https://github.com/johnno1962/injectionforxcode 118 | 119 | iOSInjectionProject/ 120 | 121 | ### Xcode ### 122 | 123 | ## Xcode 8 and earlier 124 | 125 | ### Xcode Patch ### 126 | *.xcodeproj/* 127 | !*.xcodeproj/project.pbxproj 128 | !*.xcodeproj/xcshareddata/ 129 | !*.xcodeproj/project.xcworkspace/ 130 | !*.xcworkspace/contents.xcworkspacedata 131 | /*.gcno 132 | **/xcshareddata/WorkspaceSettings.xcsettings 133 | 134 | # End of https://www.toptal.com/developers/gitignore/api/macos,xcode,swift -------------------------------------------------------------------------------- /Icon : -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aeastr/SwiftUI-AdaptiveImageGlyph/4edbe8d03596058447f630a54827cb1873737218/Icon -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Aether 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI AdaptiveImageGlyph 2 | 3 | ![Two iPhones displaying a SwiftUI application titled “SwiftUI AdaptiveImageGlyph.” On the left device, a modal or sheet with a “Cancel” button and an area featuring two emoji icons is visible. The right device shows a grid layout containing repeating emoji combinations of an angry face and a desktop computer](assets/banner.png) 4 | 5 | SwiftUI AdaptiveImageGlyph is a sample project that demonstrates how to integrate `UITextView` into a SwiftUI app to enable rich text editing with support for Adaptive Image Glyphs, including Genmojis introduced in iOS 18. 6 | 7 | ## Features 8 | - **Adaptive Image Glyph Support**: Display and interact with Apple’s Genmojis in a SwiftUI environment. 9 | - **SwiftUI Integration**: Example code for embedding using the component in SwiftUI. 10 | - **Saving to Core Data**: Includes a Core Data implementation for persistent storage. 11 | 12 | ## Origin 13 | 14 | This project is largely based on the implementation from my app **Kyo 2.0**. It demonstrates the same techniques and functionality used in the app for rich text editing and Adaptive Image Glyphs integration. Essentially, it’s a stripped-down version of Kyo 2.0's rich text editing features, made into a quick example. 15 | 16 | Check out [Kyo 2.0](https://testflight.apple.com/join/3Vm6nEBy). 17 | Check out [Kyo Discord](https://discord.gg/6NHhAvwbXV). 18 | 19 | ## Requirements 20 | - iOS 18+ 21 | - Xcode 15+ 22 | - SwiftUI 23 | 24 | ## Usage 25 | - The examples show how to use `UITextView` for handling rich text in SwiftUI. 26 | - Adaptive Image Glyphs (Genmoji) are automatically supported through the `supportsAdaptiveImageGlyph` flag. 27 | 28 | ## License 29 | 30 | This project is open-source and available under the MIT License. 31 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXFileReference section */ 10 | 2D1F1C312D283CA700AE6D60 /* SwiftUI AdaptiveImageGlyph.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftUI AdaptiveImageGlyph.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 11 | /* End PBXFileReference section */ 12 | 13 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 14 | 2D1F1C332D283CA700AE6D60 /* SwiftUI AdaptiveImageGlyph */ = { 15 | isa = PBXFileSystemSynchronizedRootGroup; 16 | path = "SwiftUI AdaptiveImageGlyph"; 17 | sourceTree = ""; 18 | }; 19 | /* End PBXFileSystemSynchronizedRootGroup section */ 20 | 21 | /* Begin PBXFrameworksBuildPhase section */ 22 | 2D1F1C2E2D283CA700AE6D60 /* Frameworks */ = { 23 | isa = PBXFrameworksBuildPhase; 24 | buildActionMask = 2147483647; 25 | files = ( 26 | ); 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXFrameworksBuildPhase section */ 30 | 31 | /* Begin PBXGroup section */ 32 | 2D1F1C282D283CA700AE6D60 = { 33 | isa = PBXGroup; 34 | children = ( 35 | 2D1F1C332D283CA700AE6D60 /* SwiftUI AdaptiveImageGlyph */, 36 | 2D1F1C322D283CA700AE6D60 /* Products */, 37 | ); 38 | sourceTree = ""; 39 | }; 40 | 2D1F1C322D283CA700AE6D60 /* Products */ = { 41 | isa = PBXGroup; 42 | children = ( 43 | 2D1F1C312D283CA700AE6D60 /* SwiftUI AdaptiveImageGlyph.app */, 44 | ); 45 | name = Products; 46 | sourceTree = ""; 47 | }; 48 | /* End PBXGroup section */ 49 | 50 | /* Begin PBXNativeTarget section */ 51 | 2D1F1C302D283CA700AE6D60 /* SwiftUI AdaptiveImageGlyph */ = { 52 | isa = PBXNativeTarget; 53 | buildConfigurationList = 2D1F1C442D283CA800AE6D60 /* Build configuration list for PBXNativeTarget "SwiftUI AdaptiveImageGlyph" */; 54 | buildPhases = ( 55 | 2D1F1C2D2D283CA700AE6D60 /* Sources */, 56 | 2D1F1C2E2D283CA700AE6D60 /* Frameworks */, 57 | 2D1F1C2F2D283CA700AE6D60 /* Resources */, 58 | ); 59 | buildRules = ( 60 | ); 61 | dependencies = ( 62 | ); 63 | fileSystemSynchronizedGroups = ( 64 | 2D1F1C332D283CA700AE6D60 /* SwiftUI AdaptiveImageGlyph */, 65 | ); 66 | name = "SwiftUI AdaptiveImageGlyph"; 67 | packageProductDependencies = ( 68 | ); 69 | productName = "SwiftUI AdaptiveImageGlyph"; 70 | productReference = 2D1F1C312D283CA700AE6D60 /* SwiftUI AdaptiveImageGlyph.app */; 71 | productType = "com.apple.product-type.application"; 72 | }; 73 | /* End PBXNativeTarget section */ 74 | 75 | /* Begin PBXProject section */ 76 | 2D1F1C292D283CA700AE6D60 /* Project object */ = { 77 | isa = PBXProject; 78 | attributes = { 79 | BuildIndependentTargetsInParallel = 1; 80 | LastSwiftUpdateCheck = 1620; 81 | LastUpgradeCheck = 1620; 82 | TargetAttributes = { 83 | 2D1F1C302D283CA700AE6D60 = { 84 | CreatedOnToolsVersion = 16.2; 85 | }; 86 | }; 87 | }; 88 | buildConfigurationList = 2D1F1C2C2D283CA700AE6D60 /* Build configuration list for PBXProject "SwiftUI AdaptiveImageGlyph" */; 89 | developmentRegion = en; 90 | hasScannedForEncodings = 0; 91 | knownRegions = ( 92 | en, 93 | Base, 94 | ); 95 | mainGroup = 2D1F1C282D283CA700AE6D60; 96 | minimizedProjectReferenceProxies = 1; 97 | preferredProjectObjectVersion = 77; 98 | productRefGroup = 2D1F1C322D283CA700AE6D60 /* Products */; 99 | projectDirPath = ""; 100 | projectRoot = ""; 101 | targets = ( 102 | 2D1F1C302D283CA700AE6D60 /* SwiftUI AdaptiveImageGlyph */, 103 | ); 104 | }; 105 | /* End PBXProject section */ 106 | 107 | /* Begin PBXResourcesBuildPhase section */ 108 | 2D1F1C2F2D283CA700AE6D60 /* Resources */ = { 109 | isa = PBXResourcesBuildPhase; 110 | buildActionMask = 2147483647; 111 | files = ( 112 | ); 113 | runOnlyForDeploymentPostprocessing = 0; 114 | }; 115 | /* End PBXResourcesBuildPhase section */ 116 | 117 | /* Begin PBXSourcesBuildPhase section */ 118 | 2D1F1C2D2D283CA700AE6D60 /* Sources */ = { 119 | isa = PBXSourcesBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | /* End PBXSourcesBuildPhase section */ 126 | 127 | /* Begin XCBuildConfiguration section */ 128 | 2D1F1C422D283CA800AE6D60 /* Debug */ = { 129 | isa = XCBuildConfiguration; 130 | buildSettings = { 131 | ALWAYS_SEARCH_USER_PATHS = NO; 132 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 133 | CLANG_ANALYZER_NONNULL = YES; 134 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 135 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 136 | CLANG_ENABLE_MODULES = YES; 137 | CLANG_ENABLE_OBJC_ARC = YES; 138 | CLANG_ENABLE_OBJC_WEAK = YES; 139 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 140 | CLANG_WARN_BOOL_CONVERSION = YES; 141 | CLANG_WARN_COMMA = YES; 142 | CLANG_WARN_CONSTANT_CONVERSION = YES; 143 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 144 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 145 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 146 | CLANG_WARN_EMPTY_BODY = YES; 147 | CLANG_WARN_ENUM_CONVERSION = YES; 148 | CLANG_WARN_INFINITE_RECURSION = YES; 149 | CLANG_WARN_INT_CONVERSION = YES; 150 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 151 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 152 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 153 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 154 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 155 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 156 | CLANG_WARN_STRICT_PROTOTYPES = YES; 157 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 158 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 159 | CLANG_WARN_UNREACHABLE_CODE = YES; 160 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 161 | COPY_PHASE_STRIP = NO; 162 | DEBUG_INFORMATION_FORMAT = dwarf; 163 | ENABLE_STRICT_OBJC_MSGSEND = YES; 164 | ENABLE_TESTABILITY = YES; 165 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 166 | GCC_C_LANGUAGE_STANDARD = gnu17; 167 | GCC_DYNAMIC_NO_PIC = NO; 168 | GCC_NO_COMMON_BLOCKS = YES; 169 | GCC_OPTIMIZATION_LEVEL = 0; 170 | GCC_PREPROCESSOR_DEFINITIONS = ( 171 | "DEBUG=1", 172 | "$(inherited)", 173 | ); 174 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 175 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 176 | GCC_WARN_UNDECLARED_SELECTOR = YES; 177 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 178 | GCC_WARN_UNUSED_FUNCTION = YES; 179 | GCC_WARN_UNUSED_VARIABLE = YES; 180 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 181 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 182 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 183 | MTL_FAST_MATH = YES; 184 | ONLY_ACTIVE_ARCH = YES; 185 | SDKROOT = iphoneos; 186 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 187 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 188 | }; 189 | name = Debug; 190 | }; 191 | 2D1F1C432D283CA800AE6D60 /* Release */ = { 192 | isa = XCBuildConfiguration; 193 | buildSettings = { 194 | ALWAYS_SEARCH_USER_PATHS = NO; 195 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 196 | CLANG_ANALYZER_NONNULL = YES; 197 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 198 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 199 | CLANG_ENABLE_MODULES = YES; 200 | CLANG_ENABLE_OBJC_ARC = YES; 201 | CLANG_ENABLE_OBJC_WEAK = YES; 202 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 203 | CLANG_WARN_BOOL_CONVERSION = YES; 204 | CLANG_WARN_COMMA = YES; 205 | CLANG_WARN_CONSTANT_CONVERSION = YES; 206 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 207 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 208 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 209 | CLANG_WARN_EMPTY_BODY = YES; 210 | CLANG_WARN_ENUM_CONVERSION = YES; 211 | CLANG_WARN_INFINITE_RECURSION = YES; 212 | CLANG_WARN_INT_CONVERSION = YES; 213 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 214 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 215 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 217 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 218 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 219 | CLANG_WARN_STRICT_PROTOTYPES = YES; 220 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 221 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 222 | CLANG_WARN_UNREACHABLE_CODE = YES; 223 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 224 | COPY_PHASE_STRIP = NO; 225 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 226 | ENABLE_NS_ASSERTIONS = NO; 227 | ENABLE_STRICT_OBJC_MSGSEND = YES; 228 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 229 | GCC_C_LANGUAGE_STANDARD = gnu17; 230 | GCC_NO_COMMON_BLOCKS = YES; 231 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 232 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 233 | GCC_WARN_UNDECLARED_SELECTOR = YES; 234 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 235 | GCC_WARN_UNUSED_FUNCTION = YES; 236 | GCC_WARN_UNUSED_VARIABLE = YES; 237 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 238 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 239 | MTL_ENABLE_DEBUG_INFO = NO; 240 | MTL_FAST_MATH = YES; 241 | SDKROOT = iphoneos; 242 | SWIFT_COMPILATION_MODE = wholemodule; 243 | VALIDATE_PRODUCT = YES; 244 | }; 245 | name = Release; 246 | }; 247 | 2D1F1C452D283CA800AE6D60 /* Debug */ = { 248 | isa = XCBuildConfiguration; 249 | buildSettings = { 250 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 251 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 252 | CODE_SIGN_STYLE = Automatic; 253 | CURRENT_PROJECT_VERSION = 1; 254 | DEVELOPMENT_ASSET_PATHS = "\"SwiftUI AdaptiveImageGlyph/Preview Content\""; 255 | DEVELOPMENT_TEAM = 3ST82B3Y8M; 256 | ENABLE_PREVIEWS = YES; 257 | GENERATE_INFOPLIST_FILE = YES; 258 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 259 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 260 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 261 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 262 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 263 | LD_RUNPATH_SEARCH_PATHS = ( 264 | "$(inherited)", 265 | "@executable_path/Frameworks", 266 | ); 267 | MARKETING_VERSION = 1.0; 268 | PRODUCT_BUNDLE_IDENTIFIER = "world.aethers.SwiftUI-AdaptiveImageGlyph"; 269 | PRODUCT_NAME = "$(TARGET_NAME)"; 270 | SWIFT_EMIT_LOC_STRINGS = YES; 271 | SWIFT_VERSION = 5.0; 272 | TARGETED_DEVICE_FAMILY = "1,2"; 273 | }; 274 | name = Debug; 275 | }; 276 | 2D1F1C462D283CA800AE6D60 /* Release */ = { 277 | isa = XCBuildConfiguration; 278 | buildSettings = { 279 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 280 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 281 | CODE_SIGN_STYLE = Automatic; 282 | CURRENT_PROJECT_VERSION = 1; 283 | DEVELOPMENT_ASSET_PATHS = "\"SwiftUI AdaptiveImageGlyph/Preview Content\""; 284 | DEVELOPMENT_TEAM = 3ST82B3Y8M; 285 | ENABLE_PREVIEWS = YES; 286 | GENERATE_INFOPLIST_FILE = YES; 287 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 288 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 289 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 290 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 291 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 292 | LD_RUNPATH_SEARCH_PATHS = ( 293 | "$(inherited)", 294 | "@executable_path/Frameworks", 295 | ); 296 | MARKETING_VERSION = 1.0; 297 | PRODUCT_BUNDLE_IDENTIFIER = "world.aethers.SwiftUI-AdaptiveImageGlyph"; 298 | PRODUCT_NAME = "$(TARGET_NAME)"; 299 | SWIFT_EMIT_LOC_STRINGS = YES; 300 | SWIFT_VERSION = 5.0; 301 | TARGETED_DEVICE_FAMILY = "1,2"; 302 | }; 303 | name = Release; 304 | }; 305 | /* End XCBuildConfiguration section */ 306 | 307 | /* Begin XCConfigurationList section */ 308 | 2D1F1C2C2D283CA700AE6D60 /* Build configuration list for PBXProject "SwiftUI AdaptiveImageGlyph" */ = { 309 | isa = XCConfigurationList; 310 | buildConfigurations = ( 311 | 2D1F1C422D283CA800AE6D60 /* Debug */, 312 | 2D1F1C432D283CA800AE6D60 /* Release */, 313 | ); 314 | defaultConfigurationIsVisible = 0; 315 | defaultConfigurationName = Release; 316 | }; 317 | 2D1F1C442D283CA800AE6D60 /* Build configuration list for PBXNativeTarget "SwiftUI AdaptiveImageGlyph" */ = { 318 | isa = XCConfigurationList; 319 | buildConfigurations = ( 320 | 2D1F1C452D283CA800AE6D60 /* Debug */, 321 | 2D1F1C462D283CA800AE6D60 /* Release */, 322 | ); 323 | defaultConfigurationIsVisible = 0; 324 | defaultConfigurationName = Release; 325 | }; 326 | /* End XCConfigurationList section */ 327 | }; 328 | rootObject = 2D1F1C292D283CA700AE6D60 /* Project object */; 329 | } 330 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph/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 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph/Composer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Composer.swift 3 | // SwiftUI AdaptiveImageGlyph 4 | // 5 | // Created by Aether on 03/01/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Composer: View { 11 | @Environment(\.managedObjectContext) private var viewContext 12 | 13 | @State private var richText = NSAttributedString(string: "", attributes: [ 14 | .font: UIFont.preferredFont(forTextStyle: .title2) 15 | ]) 16 | @Environment(\.dismiss) var dismiss 17 | var item: Item? = nil 18 | 19 | var body: some View { 20 | NavigationStack{ 21 | ScrollView{ 22 | RichTextEditor(attributedText: $richText) 23 | .overlay( 24 | RoundedRectangle(cornerRadius: 13) 25 | .strokeBorder( 26 | Color.gray.opacity(0.03), 27 | style: StrokeStyle( 28 | lineWidth: 0.6, 29 | lineCap: .round, 30 | lineJoin: .round 31 | ) 32 | ) 33 | ) 34 | .clipShape(RoundedRectangle(cornerRadius: 13)) 35 | .overlay( 36 | RoundedRectangle(cornerRadius: 13, style: .continuous) 37 | .stroke(Color.black.opacity(0.3), lineWidth: 0.7) 38 | ) 39 | 40 | .padding(.horizontal, 20) 41 | .padding(.top) 42 | } 43 | .navigationTitle("Add Item") 44 | .navigationBarTitleDisplayMode(.inline) 45 | .toolbar { 46 | ToolbarItem { 47 | Button("Save") { 48 | save() 49 | } 50 | } 51 | ToolbarItem(placement: .topBarLeading) { 52 | Button("Cancel") { 53 | dismiss() 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | private func save() { 61 | let saveItem = item ?? Item(context: viewContext) 62 | if item == nil{ 63 | saveItem.id = UUID() 64 | } 65 | saveItem.timestamp = Date() 66 | 67 | // Extract contents of text view as an attributed string 68 | let textContents = richText 69 | 70 | do { 71 | // Serialize as data for storage or transport 72 | let rtfData = try textContents.data(from: NSRange(location: 0, length: textContents.length), documentAttributes: [.documentType: NSAttributedString.DocumentType.rtfd]) 73 | 74 | saveItem.richText = rtfData 75 | }catch { 76 | let nsError = error as NSError 77 | print("Failed to create rtfData: \(nsError), \(nsError.userInfo)") 78 | } 79 | // Assign serialized data to the Core Data attribute 80 | 81 | do { 82 | try viewContext.save() 83 | dismiss() 84 | } catch { 85 | let nsError = error as NSError 86 | print("Failed to save Item: \(nsError), \(nsError.userInfo)") 87 | } 88 | } 89 | } 90 | 91 | #Preview { 92 | Composer() 93 | } 94 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SwiftUI AdaptiveImageGlyph 4 | // 5 | // Created by Aether on 03/01/2025. 6 | // 7 | 8 | import SwiftUI 9 | import CoreData 10 | 11 | struct ContentView: View { 12 | @Environment(\.managedObjectContext) private var viewContext 13 | @FetchRequest( 14 | sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], 15 | animation: .default) 16 | private var items: FetchedResults 17 | @State var showComposer: Bool = false 18 | 19 | @ViewBuilder 20 | func rowContent(item: Item) -> some View { 21 | HStack { 22 | Text(loadText(for: item) ?? "😔") 23 | .font(.title) 24 | Spacer() 25 | Text(item.timestamp!, style: .relative) 26 | .foregroundStyle(.secondary) 27 | .font(.caption) 28 | } 29 | .padding(.vertical, 5) 30 | } 31 | 32 | @ViewBuilder 33 | func rowDestination(item: Item) -> some View { 34 | ScrollView{ 35 | LazyVGrid(columns: [GridItem(.adaptive(minimum: 75, maximum: 500))], spacing: 20) { 36 | ForEach(FontStyles.allCases, id: \.self) { style in 37 | if let text = loadText(for: item, style: style.font) { 38 | Text(text) 39 | .font(style.font) 40 | } else { 41 | Text("😔").font(style.font) 42 | } 43 | } 44 | } 45 | .padding(.horizontal) 46 | } 47 | } 48 | 49 | var body: some View { 50 | NavigationStack { 51 | List { 52 | ForEach(items, id: \.id) { item in 53 | NavigationLink { 54 | rowDestination(item: item) 55 | } label: { 56 | rowContent(item: item) 57 | } 58 | } 59 | .onDelete(perform: deleteItems) 60 | } 61 | .toolbar { 62 | ToolbarItem(placement: .navigationBarTrailing) { 63 | EditButton() 64 | } 65 | ToolbarItem { 66 | Button(action: { showComposer.toggle() }) { 67 | Label("Add Item", systemImage: "plus") 68 | } 69 | } 70 | } 71 | .navigationTitle("SwiftUI AdaptiveImageGlyph") 72 | .navigationBarTitleDisplayMode(.inline) 73 | .sheet(isPresented: $showComposer) { 74 | Composer() 75 | .presentationDetents([.medium]) 76 | .presentationCornerRadius(30) 77 | } 78 | } 79 | } 80 | 81 | private func loadText(for item: Item, style: Font = .title) -> AttributedString? { 82 | if let rtfData = item.richText, rtfData.isEmpty == false { 83 | do { 84 | let textFromData = try NSAttributedString( 85 | data: rtfData, 86 | options: [.documentType: NSAttributedString.DocumentType.rtfd], 87 | documentAttributes: nil 88 | ) 89 | var attributedString = AttributedString(textFromData) 90 | 91 | if attributedString.characters.isEmpty { 92 | return nil 93 | } else { 94 | attributedString.font = style 95 | return attributedString 96 | } 97 | } catch { 98 | let nsError = error as NSError 99 | print("Failed to load rich text: \(nsError), \(nsError.userInfo)") 100 | } 101 | } else { 102 | print("No rich text available to load.") 103 | } 104 | return nil 105 | } 106 | 107 | private func addItem() { 108 | withAnimation { 109 | let newItem = Item(context: viewContext) 110 | newItem.timestamp = Date() 111 | 112 | do { 113 | try viewContext.save() 114 | } catch { 115 | // Replace this implementation with code to handle the error appropriately. 116 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 117 | let nsError = error as NSError 118 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 119 | } 120 | } 121 | } 122 | 123 | private func deleteItems(offsets: IndexSet) { 124 | withAnimation { 125 | offsets.map { items[$0] }.forEach(viewContext.delete) 126 | 127 | do { 128 | try viewContext.save() 129 | } catch { 130 | // Replace this implementation with code to handle the error appropriately. 131 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 132 | let nsError = error as NSError 133 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 134 | } 135 | } 136 | } 137 | } 138 | 139 | private let itemFormatter: DateFormatter = { 140 | let formatter = DateFormatter() 141 | formatter.dateStyle = .short 142 | formatter.timeStyle = .medium 143 | return formatter 144 | }() 145 | 146 | #Preview { 147 | ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) 148 | } 149 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph/FontStyles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontStyles.swift 3 | // SwiftUI AdaptiveImageGlyph 4 | // 5 | // Created by Aether on 03/01/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum FontStyles: CaseIterable { 11 | case custom3 12 | case custom2 13 | case custom 14 | case title 15 | case title2 16 | case title3 17 | case headline 18 | case subheadline 19 | case body 20 | case callout 21 | case caption 22 | case caption2 23 | case footnote 24 | 25 | var font: Font { 26 | switch self { 27 | case .custom3: return .system(size: 78) 28 | case .custom2: return .system(size: 65) 29 | case .custom: return .system(size: 50) 30 | case .title: return .title 31 | case .title2: return .title2 32 | case .title3: return .title3 33 | case .headline: return .headline 34 | case .subheadline: return .subheadline 35 | case .body: return .body 36 | case .callout: return .callout 37 | case .caption: return .caption 38 | case .caption2: return .caption2 39 | case .footnote: return .footnote 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph/Persistence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Persistence.swift 3 | // SwiftUI AdaptiveImageGlyph 4 | // 5 | // Created by Aether on 03/01/2025. 6 | // 7 | 8 | import CoreData 9 | 10 | struct PersistenceController { 11 | static let shared = PersistenceController() 12 | 13 | @MainActor 14 | static let preview: PersistenceController = { 15 | let result = PersistenceController(inMemory: true) 16 | let viewContext = result.container.viewContext 17 | for _ in 0..<10 { 18 | let newItem = Item(context: viewContext) 19 | newItem.timestamp = Date() 20 | } 21 | do { 22 | try viewContext.save() 23 | } catch { 24 | // Replace this implementation with code to handle the error appropriately. 25 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 26 | let nsError = error as NSError 27 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 28 | } 29 | return result 30 | }() 31 | 32 | let container: NSPersistentContainer 33 | 34 | init(inMemory: Bool = false) { 35 | container = NSPersistentContainer(name: "SwiftUI_AdaptiveImageGlyph") 36 | if inMemory { 37 | container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") 38 | } 39 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 40 | if let error = error as NSError? { 41 | // Replace this implementation with code to handle the error appropriately. 42 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 43 | 44 | /* 45 | Typical reasons for an error here include: 46 | * The parent directory does not exist, cannot be created, or disallows writing. 47 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 48 | * The device is out of space. 49 | * The store could not be migrated to the current model version. 50 | Check the error message to determine what the actual problem was. 51 | */ 52 | fatalError("Unresolved error \(error), \(error.userInfo)") 53 | } 54 | }) 55 | container.viewContext.automaticallyMergesChangesFromParent = true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph/RichTextEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RichTextEditor.swift 3 | // SwiftUI AdaptiveImageGlyph 4 | // 5 | // Created by Aether on 03/01/2025. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import SwiftUI 10 | import UIKit 11 | 12 | /// A SwiftUI wrapper for `UITextView` that supports rich text editing and emoji keyboard adaptation. 13 | struct RichTextEditor: UIViewRepresentable { 14 | @Binding var attributedText: NSAttributedString 15 | var fontSize: CGFloat = 50 16 | var onEditingChanged: ((Bool) -> Void)? = nil 17 | var onCommit: (() -> Void)? = nil 18 | 19 | // Coordinator to manage the delegate callbacks of UITextView 20 | class Coordinator: NSObject, UITextViewDelegate { 21 | var parent: RichTextEditor 22 | 23 | init(_ parent: RichTextEditor) { 24 | self.parent = parent 25 | } 26 | 27 | /// Called when the text view begins editing. 28 | func textViewDidBeginEditing(_ textView: UITextView) { 29 | parent.onEditingChanged?(true) 30 | } 31 | 32 | /// Called when the text view ends editing. 33 | func textViewDidEndEditing(_ textView: UITextView) { 34 | parent.onEditingChanged?(false) 35 | parent.onCommit?() 36 | } 37 | 38 | /// Called when the text view's content changes. 39 | func textViewDidChange(_ textView: UITextView) { 40 | // Update the `attributedText` binding only if there is a change 41 | if textView.attributedText != parent.attributedText { 42 | parent.attributedText = textView.attributedText 43 | } 44 | } 45 | } 46 | 47 | /// Creates a coordinator instance to manage the UITextView's delegate. 48 | func makeCoordinator() -> Coordinator { 49 | Coordinator(self) 50 | } 51 | 52 | /// Creates and configures the `EmojiTextView` instance. 53 | func makeUIView(context: Context) -> EmojiTextView { 54 | let textView = EmojiTextView() 55 | textView.delegate = context.coordinator // Set the delegate to the coordinator 56 | textView.isEditable = true // Allow text editing 57 | textView.isScrollEnabled = false // Disable scrolling for the text view 58 | textView.backgroundColor = .clear // Transparent background for the text view 59 | 60 | // MARK: Enable adaptive image glyphs 61 | textView.supportsAdaptiveImageGlyph = true 62 | 63 | // Set the initial attributed text 64 | textView.attributedText = attributedText 65 | 66 | // Configure initial typing attributes with font size 67 | textView.typingAttributes = [ 68 | .font: UIFont.systemFont(ofSize: fontSize) 69 | ] 70 | 71 | // Configure padding for the text container 72 | textView.textContainerInset = UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 12) 73 | 74 | return textView 75 | } 76 | 77 | /// Updates the `EmojiTextView` with new data when the SwiftUI state changes. 78 | func updateUIView(_ uiView: EmojiTextView, context: Context) { 79 | // Update the text only if the content has changed 80 | if uiView.attributedText != attributedText { 81 | uiView.attributedText = attributedText 82 | } 83 | } 84 | } 85 | 86 | /// A custom `UITextView` subclass that prioritizes the emoji keyboard when active. 87 | class EmojiTextView: UITextView { 88 | /// Provides a non-nil identifier to encourage the system to show the emoji keyboard. 89 | override var textInputContextIdentifier: String? { "" } 90 | 91 | /// Overrides the text input mode to prioritize the emoji keyboard when available. 92 | override var textInputMode: UITextInputMode? { 93 | for mode in UITextInputMode.activeInputModes { 94 | if mode.primaryLanguage == "emoji" { 95 | return mode 96 | } 97 | } 98 | // Fallback to the default input mode if no emoji keyboard is active 99 | return super.textInputMode 100 | } 101 | } 102 | #endif 103 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph/SwiftUI_AdaptiveImageGlyph.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | SwiftUI_AdaptiveImageGlyph.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph/SwiftUI_AdaptiveImageGlyph.xcdatamodeld/SwiftUI_AdaptiveImageGlyph.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftUI AdaptiveImageGlyph/SwiftUI_AdaptiveImageGlyphApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUI_AdaptiveImageGlyphApp.swift 3 | // SwiftUI AdaptiveImageGlyph 4 | // 5 | // Created by Aether on 03/01/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SwiftUI_AdaptiveImageGlyphApp: App { 12 | let persistenceController = PersistenceController.shared 13 | 14 | var body: some Scene { 15 | WindowGroup { 16 | ContentView() 17 | .environment(\.managedObjectContext, persistenceController.container.viewContext) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aeastr/SwiftUI-AdaptiveImageGlyph/4edbe8d03596058447f630a54827cb1873737218/assets/banner.png -------------------------------------------------------------------------------- /assets/genmoji.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aeastr/SwiftUI-AdaptiveImageGlyph/4edbe8d03596058447f630a54827cb1873737218/assets/genmoji.icns --------------------------------------------------------------------------------