├── .gitignore ├── EmojiTextView.podspec ├── EmojiTextView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── EmojiTextView.xcscheme ├── EmojiTextView ├── AttributedStringAnnotator.swift ├── Categories │ ├── NSRange+Hashable.swift │ ├── NSString+Range.swift │ └── UIColor+Interpolation.swift ├── EmojiController.swift ├── EmojiTextView.h ├── GradientTextHighlighter.swift ├── Match.swift ├── Resources │ ├── Info.plist │ └── emojis.json ├── TextHighlighting.swift └── TextToEmojiMapping.swift ├── Example ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist └── ViewController.swift ├── LICENSE ├── README.md └── demo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /EmojiTextView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "EmojiTextView" 3 | spec.version = "1.0" 4 | spec.summary = "Tap to swap out words with emojis. Inspired by Messages.app on iOS 10." 5 | spec.homepage = "https://github.com/fastred/EmojiTextView" 6 | spec.screenshot = "https://raw.githubusercontent.com/fastred/EmojiTextView/master/demo.gif" 7 | spec.license = "MIT" 8 | spec.author = { "Arkadiusz Holko" => "fastred@fastred.org" } 9 | spec.social_media_url = "https://twitter.com/arekholko" 10 | spec.source = { :git => "https://github.com/fastred/EmojiTextView.git", :tag => spec.version.to_s } 11 | spec.frameworks = ["UIKit"] 12 | spec.ios.deployment_target = "9.0" 13 | spec.source_files = "EmojiTextView/**/*.swift" 14 | spec.resources = ["EmojiTextView/Resources/emojis.json"] 15 | end 16 | 17 | -------------------------------------------------------------------------------- /EmojiTextView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A6199A951D3E61E10067BC74 /* TextHighlighting.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6199A941D3E61E10067BC74 /* TextHighlighting.swift */; }; 11 | A6199A961D3E63DF0067BC74 /* TextHighlighting.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6199A941D3E61E10067BC74 /* TextHighlighting.swift */; }; 12 | A69698BD1D4019B10096251D /* Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69698BC1D4019B10096251D /* Match.swift */; }; 13 | A69698BE1D401A470096251D /* Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69698BC1D4019B10096251D /* Match.swift */; }; 14 | A69698C01D401DA80096251D /* GradientTextHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69698BF1D401DA80096251D /* GradientTextHighlighter.swift */; }; 15 | A69698C11D401DAB0096251D /* GradientTextHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69698BF1D401DA80096251D /* GradientTextHighlighter.swift */; }; 16 | A69698C51D4021680096251D /* AttributedStringAnnotator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69698C41D4021680096251D /* AttributedStringAnnotator.swift */; }; 17 | A69698C61D4021900096251D /* AttributedStringAnnotator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69698C41D4021680096251D /* AttributedStringAnnotator.swift */; }; 18 | A69698E81D40DE950096251D /* NSRange+Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69698E51D40DE950096251D /* NSRange+Hashable.swift */; }; 19 | A69698E91D40DE950096251D /* NSString+Range.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69698E61D40DE950096251D /* NSString+Range.swift */; }; 20 | A69698EA1D40DE950096251D /* UIColor+Interpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69698E71D40DE950096251D /* UIColor+Interpolation.swift */; }; 21 | A69698ED1D40DE9E0096251D /* emojis.json in Resources */ = {isa = PBXBuildFile; fileRef = A69698EB1D40DE9E0096251D /* emojis.json */; }; 22 | A69698EF1D40DEAB0096251D /* NSRange+Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69698E51D40DE950096251D /* NSRange+Hashable.swift */; }; 23 | A69698F01D40DEAD0096251D /* NSString+Range.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69698E61D40DE950096251D /* NSString+Range.swift */; }; 24 | A69698F11D40DEAF0096251D /* UIColor+Interpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69698E71D40DE950096251D /* UIColor+Interpolation.swift */; }; 25 | A69698F21D40DEDA0096251D /* emojis.json in Resources */ = {isa = PBXBuildFile; fileRef = A69698EB1D40DE9E0096251D /* emojis.json */; }; 26 | A6A628F01D3EC7CC00D9FD19 /* TextToEmojiMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6A628EF1D3EC7CC00D9FD19 /* TextToEmojiMapping.swift */; }; 27 | A6A628F11D3EC7E000D9FD19 /* TextToEmojiMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6A628EF1D3EC7CC00D9FD19 /* TextToEmojiMapping.swift */; }; 28 | A6E5F2721D3AF09300879E2A /* EmojiTextView.h in Headers */ = {isa = PBXBuildFile; fileRef = A6E5F2711D3AF09300879E2A /* EmojiTextView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 29 | A6E5F2801D3AF15E00879E2A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E5F27F1D3AF15E00879E2A /* AppDelegate.swift */; }; 30 | A6E5F2821D3AF15E00879E2A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E5F2811D3AF15E00879E2A /* ViewController.swift */; }; 31 | A6E5F2851D3AF15E00879E2A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A6E5F2831D3AF15E00879E2A /* Main.storyboard */; }; 32 | A6E5F2871D3AF15E00879E2A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A6E5F2861D3AF15E00879E2A /* Assets.xcassets */; }; 33 | A6E5F28A1D3AF15E00879E2A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A6E5F2881D3AF15E00879E2A /* LaunchScreen.storyboard */; }; 34 | A6E5F2901D3BE89900879E2A /* EmojiController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E5F28F1D3BE89900879E2A /* EmojiController.swift */; }; 35 | A6E5F2911D3BE90900879E2A /* EmojiController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6E5F28F1D3BE89900879E2A /* EmojiController.swift */; }; 36 | /* End PBXBuildFile section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | A6199A941D3E61E10067BC74 /* TextHighlighting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextHighlighting.swift; sourceTree = ""; }; 40 | A69698BC1D4019B10096251D /* Match.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Match.swift; sourceTree = ""; }; 41 | A69698BF1D401DA80096251D /* GradientTextHighlighter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientTextHighlighter.swift; sourceTree = ""; }; 42 | A69698C41D4021680096251D /* AttributedStringAnnotator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedStringAnnotator.swift; sourceTree = ""; }; 43 | A69698E51D40DE950096251D /* NSRange+Hashable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSRange+Hashable.swift"; sourceTree = ""; }; 44 | A69698E61D40DE950096251D /* NSString+Range.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSString+Range.swift"; sourceTree = ""; }; 45 | A69698E71D40DE950096251D /* UIColor+Interpolation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Interpolation.swift"; sourceTree = ""; }; 46 | A69698EB1D40DE9E0096251D /* emojis.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = emojis.json; sourceTree = ""; }; 47 | A69698EC1D40DE9E0096251D /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | A6A628EF1D3EC7CC00D9FD19 /* TextToEmojiMapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextToEmojiMapping.swift; sourceTree = ""; }; 49 | A6E5F26E1D3AF09300879E2A /* EmojiTextView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EmojiTextView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | A6E5F2711D3AF09300879E2A /* EmojiTextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EmojiTextView.h; sourceTree = ""; }; 51 | A6E5F27D1D3AF15E00879E2A /* EmojiExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EmojiExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | A6E5F27F1D3AF15E00879E2A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 53 | A6E5F2811D3AF15E00879E2A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 54 | A6E5F2841D3AF15E00879E2A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 55 | A6E5F2861D3AF15E00879E2A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 56 | A6E5F2891D3AF15E00879E2A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 57 | A6E5F28B1D3AF15E00879E2A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 58 | A6E5F28F1D3BE89900879E2A /* EmojiController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiController.swift; sourceTree = ""; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | A6E5F26A1D3AF09300879E2A /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | A6E5F27A1D3AF15E00879E2A /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | /* End PBXFrameworksBuildPhase section */ 77 | 78 | /* Begin PBXGroup section */ 79 | A69698E11D40DE6B0096251D /* Categories */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | A69698E51D40DE950096251D /* NSRange+Hashable.swift */, 83 | A69698E61D40DE950096251D /* NSString+Range.swift */, 84 | A69698E71D40DE950096251D /* UIColor+Interpolation.swift */, 85 | ); 86 | path = Categories; 87 | sourceTree = ""; 88 | }; 89 | A69698E21D40DE710096251D /* Resources */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | A69698EB1D40DE9E0096251D /* emojis.json */, 93 | A69698EC1D40DE9E0096251D /* Info.plist */, 94 | ); 95 | path = Resources; 96 | sourceTree = ""; 97 | }; 98 | A6E5F2641D3AF09300879E2A = { 99 | isa = PBXGroup; 100 | children = ( 101 | A6E5F2701D3AF09300879E2A /* EmojiTextView */, 102 | A6E5F27E1D3AF15E00879E2A /* Example */, 103 | A6E5F26F1D3AF09300879E2A /* Products */, 104 | ); 105 | sourceTree = ""; 106 | }; 107 | A6E5F26F1D3AF09300879E2A /* Products */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | A6E5F26E1D3AF09300879E2A /* EmojiTextView.framework */, 111 | A6E5F27D1D3AF15E00879E2A /* EmojiExample.app */, 112 | ); 113 | name = Products; 114 | sourceTree = ""; 115 | }; 116 | A6E5F2701D3AF09300879E2A /* EmojiTextView */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | A69698E11D40DE6B0096251D /* Categories */, 120 | A69698E21D40DE710096251D /* Resources */, 121 | A6E5F2711D3AF09300879E2A /* EmojiTextView.h */, 122 | A69698C41D4021680096251D /* AttributedStringAnnotator.swift */, 123 | A6E5F28F1D3BE89900879E2A /* EmojiController.swift */, 124 | A69698BF1D401DA80096251D /* GradientTextHighlighter.swift */, 125 | A69698BC1D4019B10096251D /* Match.swift */, 126 | A6199A941D3E61E10067BC74 /* TextHighlighting.swift */, 127 | A6A628EF1D3EC7CC00D9FD19 /* TextToEmojiMapping.swift */, 128 | ); 129 | path = EmojiTextView; 130 | sourceTree = ""; 131 | }; 132 | A6E5F27E1D3AF15E00879E2A /* Example */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | A6E5F27F1D3AF15E00879E2A /* AppDelegate.swift */, 136 | A6E5F2811D3AF15E00879E2A /* ViewController.swift */, 137 | A6E5F2831D3AF15E00879E2A /* Main.storyboard */, 138 | A6E5F2861D3AF15E00879E2A /* Assets.xcassets */, 139 | A6E5F2881D3AF15E00879E2A /* LaunchScreen.storyboard */, 140 | A6E5F28B1D3AF15E00879E2A /* Info.plist */, 141 | ); 142 | path = Example; 143 | sourceTree = ""; 144 | }; 145 | /* End PBXGroup section */ 146 | 147 | /* Begin PBXHeadersBuildPhase section */ 148 | A6E5F26B1D3AF09300879E2A /* Headers */ = { 149 | isa = PBXHeadersBuildPhase; 150 | buildActionMask = 2147483647; 151 | files = ( 152 | A6E5F2721D3AF09300879E2A /* EmojiTextView.h in Headers */, 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXHeadersBuildPhase section */ 157 | 158 | /* Begin PBXNativeTarget section */ 159 | A6E5F26D1D3AF09300879E2A /* EmojiTextView */ = { 160 | isa = PBXNativeTarget; 161 | buildConfigurationList = A6E5F2761D3AF09300879E2A /* Build configuration list for PBXNativeTarget "EmojiTextView" */; 162 | buildPhases = ( 163 | A6E5F2691D3AF09300879E2A /* Sources */, 164 | A6E5F26A1D3AF09300879E2A /* Frameworks */, 165 | A6E5F26B1D3AF09300879E2A /* Headers */, 166 | A6E5F26C1D3AF09300879E2A /* Resources */, 167 | ); 168 | buildRules = ( 169 | ); 170 | dependencies = ( 171 | ); 172 | name = EmojiTextView; 173 | productName = EmojiTextView; 174 | productReference = A6E5F26E1D3AF09300879E2A /* EmojiTextView.framework */; 175 | productType = "com.apple.product-type.framework"; 176 | }; 177 | A6E5F27C1D3AF15E00879E2A /* EmojiExample */ = { 178 | isa = PBXNativeTarget; 179 | buildConfigurationList = A6E5F28C1D3AF15E00879E2A /* Build configuration list for PBXNativeTarget "EmojiExample" */; 180 | buildPhases = ( 181 | A6E5F2791D3AF15E00879E2A /* Sources */, 182 | A6E5F27A1D3AF15E00879E2A /* Frameworks */, 183 | A6E5F27B1D3AF15E00879E2A /* Resources */, 184 | ); 185 | buildRules = ( 186 | ); 187 | dependencies = ( 188 | ); 189 | name = EmojiExample; 190 | productName = Example; 191 | productReference = A6E5F27D1D3AF15E00879E2A /* EmojiExample.app */; 192 | productType = "com.apple.product-type.application"; 193 | }; 194 | /* End PBXNativeTarget section */ 195 | 196 | /* Begin PBXProject section */ 197 | A6E5F2651D3AF09300879E2A /* Project object */ = { 198 | isa = PBXProject; 199 | attributes = { 200 | LastSwiftUpdateCheck = 0730; 201 | LastUpgradeCheck = 0800; 202 | ORGANIZATIONNAME = "Arkadiusz Holko"; 203 | TargetAttributes = { 204 | A6E5F26D1D3AF09300879E2A = { 205 | CreatedOnToolsVersion = 7.3.1; 206 | }; 207 | A6E5F27C1D3AF15E00879E2A = { 208 | CreatedOnToolsVersion = 7.3.1; 209 | DevelopmentTeam = JNVZW2VES4; 210 | LastSwiftMigration = 0800; 211 | }; 212 | }; 213 | }; 214 | buildConfigurationList = A6E5F2681D3AF09300879E2A /* Build configuration list for PBXProject "EmojiTextView" */; 215 | compatibilityVersion = "Xcode 3.2"; 216 | developmentRegion = English; 217 | hasScannedForEncodings = 0; 218 | knownRegions = ( 219 | en, 220 | Base, 221 | ); 222 | mainGroup = A6E5F2641D3AF09300879E2A; 223 | productRefGroup = A6E5F26F1D3AF09300879E2A /* Products */; 224 | projectDirPath = ""; 225 | projectRoot = ""; 226 | targets = ( 227 | A6E5F26D1D3AF09300879E2A /* EmojiTextView */, 228 | A6E5F27C1D3AF15E00879E2A /* EmojiExample */, 229 | ); 230 | }; 231 | /* End PBXProject section */ 232 | 233 | /* Begin PBXResourcesBuildPhase section */ 234 | A6E5F26C1D3AF09300879E2A /* Resources */ = { 235 | isa = PBXResourcesBuildPhase; 236 | buildActionMask = 2147483647; 237 | files = ( 238 | A69698ED1D40DE9E0096251D /* emojis.json in Resources */, 239 | ); 240 | runOnlyForDeploymentPostprocessing = 0; 241 | }; 242 | A6E5F27B1D3AF15E00879E2A /* Resources */ = { 243 | isa = PBXResourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | A69698F21D40DEDA0096251D /* emojis.json in Resources */, 247 | A6E5F28A1D3AF15E00879E2A /* LaunchScreen.storyboard in Resources */, 248 | A6E5F2871D3AF15E00879E2A /* Assets.xcassets in Resources */, 249 | A6E5F2851D3AF15E00879E2A /* Main.storyboard in Resources */, 250 | ); 251 | runOnlyForDeploymentPostprocessing = 0; 252 | }; 253 | /* End PBXResourcesBuildPhase section */ 254 | 255 | /* Begin PBXSourcesBuildPhase section */ 256 | A6E5F2691D3AF09300879E2A /* Sources */ = { 257 | isa = PBXSourcesBuildPhase; 258 | buildActionMask = 2147483647; 259 | files = ( 260 | A69698E81D40DE950096251D /* NSRange+Hashable.swift in Sources */, 261 | A6199A951D3E61E10067BC74 /* TextHighlighting.swift in Sources */, 262 | A6E5F2901D3BE89900879E2A /* EmojiController.swift in Sources */, 263 | A69698EA1D40DE950096251D /* UIColor+Interpolation.swift in Sources */, 264 | A69698C01D401DA80096251D /* GradientTextHighlighter.swift in Sources */, 265 | A6A628F01D3EC7CC00D9FD19 /* TextToEmojiMapping.swift in Sources */, 266 | A69698BD1D4019B10096251D /* Match.swift in Sources */, 267 | A69698C51D4021680096251D /* AttributedStringAnnotator.swift in Sources */, 268 | A69698E91D40DE950096251D /* NSString+Range.swift in Sources */, 269 | ); 270 | runOnlyForDeploymentPostprocessing = 0; 271 | }; 272 | A6E5F2791D3AF15E00879E2A /* Sources */ = { 273 | isa = PBXSourcesBuildPhase; 274 | buildActionMask = 2147483647; 275 | files = ( 276 | A6199A961D3E63DF0067BC74 /* TextHighlighting.swift in Sources */, 277 | A69698F01D40DEAD0096251D /* NSString+Range.swift in Sources */, 278 | A69698BE1D401A470096251D /* Match.swift in Sources */, 279 | A6E5F2821D3AF15E00879E2A /* ViewController.swift in Sources */, 280 | A6A628F11D3EC7E000D9FD19 /* TextToEmojiMapping.swift in Sources */, 281 | A6E5F2801D3AF15E00879E2A /* AppDelegate.swift in Sources */, 282 | A6E5F2911D3BE90900879E2A /* EmojiController.swift in Sources */, 283 | A69698EF1D40DEAB0096251D /* NSRange+Hashable.swift in Sources */, 284 | A69698C11D401DAB0096251D /* GradientTextHighlighter.swift in Sources */, 285 | A69698F11D40DEAF0096251D /* UIColor+Interpolation.swift in Sources */, 286 | A69698C61D4021900096251D /* AttributedStringAnnotator.swift in Sources */, 287 | ); 288 | runOnlyForDeploymentPostprocessing = 0; 289 | }; 290 | /* End PBXSourcesBuildPhase section */ 291 | 292 | /* Begin PBXVariantGroup section */ 293 | A6E5F2831D3AF15E00879E2A /* Main.storyboard */ = { 294 | isa = PBXVariantGroup; 295 | children = ( 296 | A6E5F2841D3AF15E00879E2A /* Base */, 297 | ); 298 | name = Main.storyboard; 299 | sourceTree = ""; 300 | }; 301 | A6E5F2881D3AF15E00879E2A /* LaunchScreen.storyboard */ = { 302 | isa = PBXVariantGroup; 303 | children = ( 304 | A6E5F2891D3AF15E00879E2A /* Base */, 305 | ); 306 | name = LaunchScreen.storyboard; 307 | sourceTree = ""; 308 | }; 309 | /* End PBXVariantGroup section */ 310 | 311 | /* Begin XCBuildConfiguration section */ 312 | A6E5F2741D3AF09300879E2A /* Debug */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | ALWAYS_SEARCH_USER_PATHS = NO; 316 | CLANG_ANALYZER_NONNULL = YES; 317 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 318 | CLANG_CXX_LIBRARY = "libc++"; 319 | CLANG_ENABLE_MODULES = YES; 320 | CLANG_ENABLE_OBJC_ARC = YES; 321 | CLANG_WARN_BOOL_CONVERSION = YES; 322 | CLANG_WARN_CONSTANT_CONVERSION = YES; 323 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 324 | CLANG_WARN_EMPTY_BODY = YES; 325 | CLANG_WARN_ENUM_CONVERSION = YES; 326 | CLANG_WARN_INFINITE_RECURSION = YES; 327 | CLANG_WARN_INT_CONVERSION = YES; 328 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 329 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 330 | CLANG_WARN_UNREACHABLE_CODE = YES; 331 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 332 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 333 | COPY_PHASE_STRIP = NO; 334 | CURRENT_PROJECT_VERSION = 1; 335 | DEBUG_INFORMATION_FORMAT = dwarf; 336 | ENABLE_STRICT_OBJC_MSGSEND = YES; 337 | ENABLE_TESTABILITY = YES; 338 | GCC_C_LANGUAGE_STANDARD = gnu99; 339 | GCC_DYNAMIC_NO_PIC = NO; 340 | GCC_NO_COMMON_BLOCKS = YES; 341 | GCC_OPTIMIZATION_LEVEL = 0; 342 | GCC_PREPROCESSOR_DEFINITIONS = ( 343 | "DEBUG=1", 344 | "$(inherited)", 345 | ); 346 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 347 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 348 | GCC_WARN_UNDECLARED_SELECTOR = YES; 349 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 350 | GCC_WARN_UNUSED_FUNCTION = YES; 351 | GCC_WARN_UNUSED_VARIABLE = YES; 352 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 353 | MTL_ENABLE_DEBUG_INFO = YES; 354 | ONLY_ACTIVE_ARCH = YES; 355 | SDKROOT = iphoneos; 356 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 357 | TARGETED_DEVICE_FAMILY = "1,2"; 358 | VERSIONING_SYSTEM = "apple-generic"; 359 | VERSION_INFO_PREFIX = ""; 360 | }; 361 | name = Debug; 362 | }; 363 | A6E5F2751D3AF09300879E2A /* Release */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | ALWAYS_SEARCH_USER_PATHS = NO; 367 | CLANG_ANALYZER_NONNULL = YES; 368 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 369 | CLANG_CXX_LIBRARY = "libc++"; 370 | CLANG_ENABLE_MODULES = YES; 371 | CLANG_ENABLE_OBJC_ARC = YES; 372 | CLANG_WARN_BOOL_CONVERSION = YES; 373 | CLANG_WARN_CONSTANT_CONVERSION = YES; 374 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 375 | CLANG_WARN_EMPTY_BODY = YES; 376 | CLANG_WARN_ENUM_CONVERSION = YES; 377 | CLANG_WARN_INFINITE_RECURSION = YES; 378 | CLANG_WARN_INT_CONVERSION = YES; 379 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 380 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 381 | CLANG_WARN_UNREACHABLE_CODE = YES; 382 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 383 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 384 | COPY_PHASE_STRIP = NO; 385 | CURRENT_PROJECT_VERSION = 1; 386 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 387 | ENABLE_NS_ASSERTIONS = NO; 388 | ENABLE_STRICT_OBJC_MSGSEND = YES; 389 | GCC_C_LANGUAGE_STANDARD = gnu99; 390 | GCC_NO_COMMON_BLOCKS = YES; 391 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 392 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 393 | GCC_WARN_UNDECLARED_SELECTOR = YES; 394 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 395 | GCC_WARN_UNUSED_FUNCTION = YES; 396 | GCC_WARN_UNUSED_VARIABLE = YES; 397 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 398 | MTL_ENABLE_DEBUG_INFO = NO; 399 | SDKROOT = iphoneos; 400 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 401 | TARGETED_DEVICE_FAMILY = "1,2"; 402 | VALIDATE_PRODUCT = YES; 403 | VERSIONING_SYSTEM = "apple-generic"; 404 | VERSION_INFO_PREFIX = ""; 405 | }; 406 | name = Release; 407 | }; 408 | A6E5F2771D3AF09300879E2A /* Debug */ = { 409 | isa = XCBuildConfiguration; 410 | buildSettings = { 411 | CLANG_ENABLE_MODULES = YES; 412 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 413 | DEFINES_MODULE = YES; 414 | DYLIB_COMPATIBILITY_VERSION = 1; 415 | DYLIB_CURRENT_VERSION = 1; 416 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 417 | INFOPLIST_FILE = EmojiTextView/Resources/Info.plist; 418 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 419 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 420 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 421 | PRODUCT_BUNDLE_IDENTIFIER = pl.holko.EmojiTextView; 422 | PRODUCT_NAME = "$(TARGET_NAME)"; 423 | SKIP_INSTALL = YES; 424 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 425 | }; 426 | name = Debug; 427 | }; 428 | A6E5F2781D3AF09300879E2A /* Release */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | CLANG_ENABLE_MODULES = YES; 432 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 433 | DEFINES_MODULE = YES; 434 | DYLIB_COMPATIBILITY_VERSION = 1; 435 | DYLIB_CURRENT_VERSION = 1; 436 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 437 | INFOPLIST_FILE = EmojiTextView/Resources/Info.plist; 438 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 439 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 440 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 441 | PRODUCT_BUNDLE_IDENTIFIER = pl.holko.EmojiTextView; 442 | PRODUCT_NAME = "$(TARGET_NAME)"; 443 | SKIP_INSTALL = YES; 444 | }; 445 | name = Release; 446 | }; 447 | A6E5F28D1D3AF15E00879E2A /* Debug */ = { 448 | isa = XCBuildConfiguration; 449 | buildSettings = { 450 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 451 | CODE_SIGN_IDENTITY = "iPhone Developer"; 452 | INFOPLIST_FILE = Example/Info.plist; 453 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 454 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 455 | PRODUCT_BUNDLE_IDENTIFIER = pl.holko.Example; 456 | PRODUCT_NAME = "$(TARGET_NAME)"; 457 | SWIFT_VERSION = 3.0; 458 | }; 459 | name = Debug; 460 | }; 461 | A6E5F28E1D3AF15E00879E2A /* Release */ = { 462 | isa = XCBuildConfiguration; 463 | buildSettings = { 464 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 465 | CODE_SIGN_IDENTITY = "iPhone Developer"; 466 | INFOPLIST_FILE = Example/Info.plist; 467 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 468 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 469 | PRODUCT_BUNDLE_IDENTIFIER = pl.holko.Example; 470 | PRODUCT_NAME = "$(TARGET_NAME)"; 471 | SWIFT_VERSION = 3.0; 472 | }; 473 | name = Release; 474 | }; 475 | /* End XCBuildConfiguration section */ 476 | 477 | /* Begin XCConfigurationList section */ 478 | A6E5F2681D3AF09300879E2A /* Build configuration list for PBXProject "EmojiTextView" */ = { 479 | isa = XCConfigurationList; 480 | buildConfigurations = ( 481 | A6E5F2741D3AF09300879E2A /* Debug */, 482 | A6E5F2751D3AF09300879E2A /* Release */, 483 | ); 484 | defaultConfigurationIsVisible = 0; 485 | defaultConfigurationName = Release; 486 | }; 487 | A6E5F2761D3AF09300879E2A /* Build configuration list for PBXNativeTarget "EmojiTextView" */ = { 488 | isa = XCConfigurationList; 489 | buildConfigurations = ( 490 | A6E5F2771D3AF09300879E2A /* Debug */, 491 | A6E5F2781D3AF09300879E2A /* Release */, 492 | ); 493 | defaultConfigurationIsVisible = 0; 494 | defaultConfigurationName = Release; 495 | }; 496 | A6E5F28C1D3AF15E00879E2A /* Build configuration list for PBXNativeTarget "EmojiExample" */ = { 497 | isa = XCConfigurationList; 498 | buildConfigurations = ( 499 | A6E5F28D1D3AF15E00879E2A /* Debug */, 500 | A6E5F28E1D3AF15E00879E2A /* Release */, 501 | ); 502 | defaultConfigurationIsVisible = 0; 503 | defaultConfigurationName = Release; 504 | }; 505 | /* End XCConfigurationList section */ 506 | }; 507 | rootObject = A6E5F2651D3AF09300879E2A /* Project object */; 508 | } 509 | -------------------------------------------------------------------------------- /EmojiTextView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /EmojiTextView.xcodeproj/xcshareddata/xcschemes/EmojiTextView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /EmojiTextView/AttributedStringAnnotator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedStringAnnotator.swift 3 | // EmojiTextView 4 | // 5 | // Created by Arkadiusz Holko on 20/07/16. 6 | // Copyright © 2016 Arkadiusz Holko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class AttributedStringAnnotator { 12 | 13 | let mapping: TextToEmojiMapping 14 | let defaultAttributes: [String : AnyObject] 15 | let annotationKey: String 16 | 17 | init(mapping: TextToEmojiMapping, defaultAttributes: [String : AnyObject], annotationKey: String) { 18 | self.mapping = mapping 19 | self.defaultAttributes = defaultAttributes 20 | self.annotationKey = annotationKey 21 | } 22 | 23 | // Returns the string with annotationKey annotation for words that can be replaced with emojis. 24 | func annotatedAttributedString(fromAttributedString: NSAttributedString) -> (NSAttributedString, Bool) { 25 | 26 | let string = fromAttributedString.string as NSString 27 | let mutable = fromAttributedString.mutableCopy() as! NSMutableAttributedString 28 | 29 | var rangeToMatchMap: [NSRange : Match] = [:] 30 | var shouldCancelRunningHighlighter = false 31 | 32 | // Adds annotationKey attribute to all words in the string that can be replaced with emojis. 33 | string.enumerateSubstrings(in: string.range, options: .byWords) { (substring, substringRange, enclosingRange, stop) in 34 | 35 | if let substring = substring, 36 | let mapped = self.mapping[(substring as NSString).lowercased]?.first { 37 | 38 | let match = Match(string: substring as NSString, emoji: mapped as NSString) 39 | rangeToMatchMap[substringRange] = match 40 | 41 | let existingAttribute = mutable.attribute(self.annotationKey, at: substringRange.location, effectiveRange: nil) 42 | 43 | if existingAttribute == nil { 44 | mutable.addAttribute(self.annotationKey, value: match, range: substringRange) 45 | } 46 | } 47 | } 48 | 49 | // Removes annotationKey attribute from parts of the string that were replaceable with emoji but aren't anymore. 50 | mutable.enumerateAttribute(annotationKey, in: string.range, options: []) { (value, range, stop) in 51 | guard let match = value as? Match else { return } 52 | 53 | if rangeToMatchMap[range] == nil { 54 | if match.transitionState == .running { 55 | shouldCancelRunningHighlighter = true 56 | } 57 | 58 | mutable.removeAttribute(self.annotationKey, range: range) 59 | } 60 | } 61 | 62 | // Sets default formatting for parts of the string that don't have annotationKey attribute associated with them. 63 | for i in 0.. Bool { 20 | return lhs.location == rhs.location && lhs.length == rhs.length 21 | } 22 | -------------------------------------------------------------------------------- /EmojiTextView/Categories/NSString+Range.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+Range.swift 3 | // EmojiTextView 4 | // 5 | // Created by Arkadiusz Holko on 20/07/16. 6 | // Copyright © 2016 Arkadiusz Holko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSString { 12 | public var range: NSRange { 13 | return NSRange(location: 0, length: self.length) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EmojiTextView/Categories/UIColor+Interpolation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Interpolation.swift 3 | // EmojiTextView 4 | // 5 | // Created by Arkadiusz Holko on 20/07/16. 6 | // Copyright © 2016 Arkadiusz Holko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIColor { 13 | func interpolateColorTo(_ endColor: UIColor, progress: CGFloat) -> UIColor { 14 | var f = max(0, progress) 15 | f = min(1, progress) 16 | 17 | var h1: CGFloat = 0 18 | var s1: CGFloat = 0 19 | var b1: CGFloat = 0 20 | var a1: CGFloat = 0 21 | self.getHue(&h1, saturation: &s1, brightness: &b1, alpha: &a1) 22 | 23 | var h2: CGFloat = 0 24 | var s2: CGFloat = 0 25 | var b2: CGFloat = 0 26 | var a2: CGFloat = 0 27 | endColor.getHue(&h2, saturation: &s2, brightness: &b2, alpha: &a2) 28 | 29 | let h = h1 + (h2-h1) * f 30 | let s = s1 + (s2-s1) * f 31 | let b = b1 + (b2-b1) * f 32 | let a = a1 + (a2-a1) * f 33 | 34 | return UIColor(hue: h, saturation: s, brightness: b, alpha: a) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /EmojiTextView/EmojiController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiController.swift 3 | // EmojiTextView 4 | // 5 | // Created by Arkadiusz Holko on 17/07/16. 6 | // Copyright © 2016 Arkadiusz Holko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | 13 | final public class EmojiController: NSObject { 14 | 15 | fileprivate let textView: UITextView 16 | fileprivate let tapRecognizer: UITapGestureRecognizer 17 | fileprivate var annotator: AttributedStringAnnotator 18 | fileprivate static let emojiKey = "EmojiTextViewKey" 19 | 20 | // text to emoji mapping 21 | public var mapping: TextToEmojiMapping = defaultTextToEmojiMapping() 22 | 23 | // object responsible for highlighting the text that is replaceable with emoji 24 | fileprivate var currentTextHighlighter: TextHighlighting? 25 | // returns a new instance of `TextHighlighting` object 26 | public var textHighlightingFactory: () -> TextHighlighting = { 27 | return GradientTextHighlighter() 28 | } 29 | 30 | // NSAttributedString attributes for the regular text 31 | public var defaultAttributes: [String : AnyObject] = { 32 | return [NSForegroundColorAttributeName : UIColor.black, 33 | NSFontAttributeName : UIFont.systemFont(ofSize: 18)] 34 | }() 35 | 36 | public init(textView: UITextView) { 37 | self.textView = textView 38 | self.tapRecognizer = UITapGestureRecognizer() 39 | self.annotator = AttributedStringAnnotator(mapping: mapping, defaultAttributes: defaultAttributes, annotationKey: type(of: self).emojiKey) 40 | 41 | super.init() 42 | 43 | NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name:NSNotification.Name.UITextViewTextDidChange, object: textView) 44 | 45 | tapRecognizer.addTarget(self, action: #selector(didTap(_:))) 46 | tapRecognizer.delegate = self 47 | textView.addGestureRecognizer(tapRecognizer) 48 | textView.typingAttributes = defaultAttributes 49 | 50 | updateAfterTextChange() 51 | } 52 | 53 | deinit { 54 | currentTextHighlighter?.cancel() 55 | } 56 | 57 | @objc fileprivate func textDidChange(_ notification: Notification) { 58 | updateAfterTextChange() 59 | } 60 | 61 | fileprivate func updateAfterTextChange() { 62 | let (annotatedAttributedString, shouldCancel) = annotator.annotatedAttributedString(fromAttributedString: textView.attributedText) 63 | if shouldCancel { 64 | currentTextHighlighter?.cancel() 65 | } 66 | 67 | let selectedRange = textView.selectedRange 68 | textView.attributedText = annotatedAttributedString 69 | textView.selectedRange = selectedRange 70 | 71 | highlightIfNeeded() 72 | } 73 | 74 | fileprivate func highlightIfNeeded() { 75 | let string = textView.attributedText.string as NSString 76 | var toTransition: (NSRange, Match)? 77 | 78 | // Sets toTransition with first not started match unless some transition is currently running. 79 | textView.attributedText.enumerateAttribute(type(of: self).emojiKey, in: string.range, options: []) { (value, range, stop) in 80 | 81 | guard let match = value as? Match else { return } 82 | if match.transitionState == .running { 83 | stop.pointee = true 84 | } else if match.transitionState == .notStarted { 85 | toTransition = (range, match) 86 | stop.pointee = true 87 | } 88 | } 89 | 90 | if let (range, match) = toTransition { 91 | let textHighlighter = textHighlightingFactory() 92 | currentTextHighlighter = textHighlighter 93 | match.transitionState = .running 94 | 95 | textHighlighter.highlight(range: range, inTextView: textView, completion: { [weak self] (finished) in 96 | match.transitionState = .completed 97 | self?.highlightIfNeeded() 98 | }) 99 | } 100 | } 101 | 102 | @objc fileprivate func didTap(_ gestureRecognizer: UITapGestureRecognizer) { 103 | if let (range, match) = rangeAndMatch(atPoint: gestureRecognizer.location(in: textView)) , match.transitionState == .completed { 104 | let mutable = textView.attributedText.mutableCopy() as! NSMutableAttributedString 105 | mutable.replaceCharacters(in: range, with: match.emoji as String) 106 | textView.attributedText = mutable.copy() as! NSAttributedString 107 | 108 | updateAfterTextChange() 109 | } 110 | } 111 | 112 | fileprivate func rangeAndMatch(atPoint: CGPoint) -> (NSRange, Match)? { 113 | guard !textView.attributedText.string.isEmpty else { 114 | return nil 115 | } 116 | 117 | let layoutManager = textView.layoutManager 118 | let characterIndex = layoutManager.characterIndex(for: atPoint, in: textView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil) 119 | 120 | var longestRange: NSRange = NSMakeRange(0, 0) 121 | let attributes = textView.attributedText.attributes(at: characterIndex, longestEffectiveRange: &longestRange, in: (textView.attributedText.string as NSString).range) 122 | 123 | if NSLocationInRange(characterIndex, longestRange) { 124 | if let match = attributes[type(of: self).emojiKey] as? Match { 125 | return (longestRange, match) 126 | } 127 | } 128 | 129 | return nil 130 | } 131 | } 132 | 133 | extension EmojiController: UIGestureRecognizerDelegate { 134 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 135 | return true 136 | } 137 | 138 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { 139 | 140 | // Prevents visual glitches by disabling default UITextView's gesture recognizers 141 | // from working on words that can be replaced with emojis. 142 | return rangeAndMatch(atPoint: gestureRecognizer.location(in: textView)) != nil 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /EmojiTextView/EmojiTextView.h: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiTextView.h 3 | // EmojiTextView 4 | // 5 | // Created by Arkadiusz Holko on 17/07/16. 6 | // Copyright © 2016 Arkadiusz Holko. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for EmojiTextView. 12 | FOUNDATION_EXPORT double EmojiTextViewVersionNumber; 13 | 14 | //! Project version string for EmojiTextView. 15 | FOUNDATION_EXPORT const unsigned char EmojiTextViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /EmojiTextView/GradientTextHighlighter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientTextHighlighter.swift 3 | // EmojiTextView 4 | // 5 | // Created by Arkadiusz Holko on 20/07/16. 6 | // Copyright © 2016 Arkadiusz Holko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | open class GradientTextHighlighter: TextHighlighting { 13 | 14 | fileprivate var displayLink: CADisplayLink? 15 | 16 | open var animationDurationPerLetter: CGFloat = 0.06 17 | open var animationOverlap: CGFloat = 0.04 18 | 19 | fileprivate var startTime: CFTimeInterval = 0.0 20 | weak fileprivate var textView: UITextView? 21 | fileprivate var range: NSRange? 22 | fileprivate var completion: ((Bool) -> ())? 23 | 24 | open func highlight(range: NSRange, inTextView textView: UITextView, completion: @escaping (_ finished: Bool) -> ()) { 25 | self.textView = textView 26 | self.range = range 27 | self.completion = completion 28 | 29 | // Animation of text on iOS is in the dire state. There doesn't seem to any other way of animating text 30 | // that works with the default stack of UITextView. 31 | displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidFire(_:))) 32 | displayLink?.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode) 33 | startTime = CACurrentMediaTime() 34 | } 35 | 36 | deinit { 37 | stopDisplayLink() 38 | } 39 | 40 | open func cancel() { 41 | stopDisplayLink() 42 | } 43 | 44 | fileprivate func stopDisplayLink() { 45 | displayLink?.remove(from: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode) 46 | displayLink = nil 47 | } 48 | 49 | @objc fileprivate func displayLinkDidFire(_ displayLink: CADisplayLink) { 50 | guard let textView = textView, let range = range else { 51 | return 52 | } 53 | 54 | for i in 0..= 1.0 { 74 | stopDisplayLink() 75 | completion?(true) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /EmojiTextView/Match.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Match.swift 3 | // EmojiTextView 4 | // 5 | // Created by Arkadiusz Holko on 20/07/16. 6 | // Copyright © 2016 Arkadiusz Holko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `Match` class keeps information about a single word that can be replaced with an emoji. 12 | class Match { 13 | /// State of the animated transition (highlighting). 14 | enum TransitionState { 15 | case notStarted 16 | case running 17 | case completed 18 | } 19 | 20 | let string: NSString 21 | let emoji: NSString 22 | var transitionState = TransitionState.notStarted 23 | 24 | init(string: NSString, emoji: NSString) { 25 | self.string = string 26 | self.emoji = emoji 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /EmojiTextView/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /EmojiTextView/TextHighlighting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextHighlighting.swift 3 | // EmojiTextView 4 | // 5 | // Created by Arkadiusz Holko on 19/07/16. 6 | // Copyright © 2016 Arkadiusz Holko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | // Object responsible for handling the highlighting of the text that's replaceable with an emoji. 13 | public protocol TextHighlighting { 14 | 15 | // Highlights range of text in the text view that's replaceable with an emoji. completion closure has to be called when the highlighting finishes. 16 | func highlight(range: NSRange, inTextView textView: UITextView, completion: @escaping (_ finished: Bool) -> ()) 17 | 18 | // Cancels highlighting that's in progress 19 | func cancel() 20 | } 21 | -------------------------------------------------------------------------------- /EmojiTextView/TextToEmojiMapping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextToEmojiMapping.swift 3 | // EmojiTextView 4 | // 5 | // Created by Arkadiusz Holko on 19/07/16. 6 | // Copyright © 2016 Arkadiusz Holko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Mapping of the regular text to emoji characters 12 | public typealias TextToEmojiMapping = [String : [String]] 13 | 14 | public func defaultTextToEmojiMapping() -> TextToEmojiMapping { 15 | 16 | var mapping: TextToEmojiMapping = [:] 17 | 18 | func addKey(_ key: String, value: String, atBeginning: Bool) { 19 | // ignore short words because they're non-essential 20 | guard key.lengthOfBytes(using: String.Encoding.utf8) > 2 else { 21 | return 22 | } 23 | 24 | if mapping[key] == nil { 25 | mapping[key] = [] 26 | } 27 | 28 | if atBeginning { 29 | mapping[key]?.insert(value, at: 0) 30 | } else { 31 | mapping[key]?.append(value) 32 | } 33 | } 34 | 35 | guard let path = Bundle(for: EmojiController.self).path(forResource: "emojis", ofType: "json"), 36 | let data = try? Data(contentsOf: URL(fileURLWithPath: path)), 37 | let json = try? JSONSerialization.jsonObject(with: data, options: []), 38 | let jsonDictionary = json as? NSDictionary else { 39 | return [:] 40 | } 41 | 42 | for (key, value) in jsonDictionary { 43 | if let key = key as? String, 44 | let dictionary = value as? Dictionary, 45 | let emojiCharacter = dictionary["char"] as? String { 46 | 47 | // Dictionary keys from emojis.json have higher priority then keywords. 48 | // That's why they're added at the beginning of the array. 49 | addKey(key, value: emojiCharacter, atBeginning: true) 50 | 51 | if let keywords = dictionary["keywords"] as? [String] { 52 | for keyword in keywords { 53 | addKey(keyword, value: emojiCharacter, atBeginning: false) 54 | } 55 | } 56 | } 57 | } 58 | 59 | return mapping 60 | } 61 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Arkadiusz Holko on 17/07/16. 6 | // Copyright © 2016 Arkadiusz Holko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | EmojiTextView allows you to swap out words with emoji with a simple tap. Words that can be replaced are highlighted, like this: Apple, iPhone. 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by Arkadiusz Holko on 17/07/16. 6 | // Copyright © 2016 Arkadiusz Holko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | var emojiController: EmojiController? 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | emojiController = EmojiController(textView: self.textView) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Arkadiusz Holko 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 | # EmojiTextView 2 | 3 | Tap to swap out words with emojis. Works with any `UITextView`. Heavily inspired by Messages.app on iOS 10. 4 | 5 | Created by [Arkadiusz Holko][holko] ([@arekholko][twitter]). 6 | 7 | ![Demo GIF](https://raw.githubusercontent.com/fastred/EmojiTextView/master/demo.gif) 8 | 9 | ## Usage 10 | 11 | Add a property of `EmojiController` type to a class that holds your `UITextView` instance, e.g. a view controller: 12 | 13 | ```swift 14 | var emojiController: EmojiController? 15 | ``` 16 | 17 | Then, initialize `EmojiController` by passing it your text view (e.g. in `viewDidLoad()`): 18 | 19 | ```swift 20 | emojiController = EmojiController(textView: textView) 21 | ``` 22 | 23 | That's it! 🎉 24 | 25 | ## Customization 26 | 27 | `EmojiController` provides three points of customization through properties: 28 | 29 | - `mapping` – contains a mapping from words to an array of emojis 30 | - `textHighlightingFactory` – creates a new instance of an object conforming to `TextHighlighting` protocol; each instance of that object is responsible for highlighting a single word 31 | - `defaultAttributes` - attributes (as in `NSAttributedString`) of a text that's not replaceable with emoji 32 | 33 | ## Installation 34 | 35 | EmojiTextView is available through [CocoaPods](http://cocoapods.org). To install it simply add the following line to your Podfile: 36 | 37 | ``` 38 | pod "EmojiTextView", "0.0.1" 39 | ``` 40 | 41 | Then you can import it with: 42 | 43 | ```swift 44 | import EmojiTextView 45 | ``` 46 | 47 | ## Requirements 48 | 49 | iOS 9 and above. 50 | 51 | ## Future Improvements 52 | 53 | - Should the emoji replacement be enabled only when the emoji keyboard is selected? It probably requires the use of the private API as `UITextInputMode` doesn't help here. 54 | - If there's more than one emoji match for a given word there should be an ability to choose which one we want to use. 55 | - **(EASY)** There should be an option to switch back from an emoji to the full word. Hint: add an attribute with the original word to the part of the string replaced by an emoji. 56 | 57 | ## Credits 58 | 59 | - Emoji keyword library is based on [emojilib][emojilib]. 60 | 61 | [emojilib]: https://github.com/muan/emojilib 62 | [holko]: http://holko.pl 63 | [twitter]: https://twitter.com/arekholko 64 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastred/EmojiTextView/4863e6a723f8dfe908340aebdda401c495ba13d3/demo.gif --------------------------------------------------------------------------------