├── .gitignore ├── .swift-version ├── ACBTokenField.podspec ├── ACBTokenField.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── ACBTokenField.xcscheme │ └── ACBTokenFieldFramework.xcscheme ├── ACBTokenField ├── Classes │ ├── General │ │ └── AppDelegate.swift │ └── ViewControllers │ │ └── ViewController.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-128x128.png │ │ │ ├── Icon-App-128x128@2x.png │ │ │ ├── Icon-App-16x16.png │ │ │ ├── Icon-App-16x16@2x.png │ │ │ ├── Icon-App-256x256.png │ │ │ ├── Icon-App-256x256@2x.png │ │ │ ├── Icon-App-32x32.png │ │ │ ├── Icon-App-32x32@2x.png │ │ │ ├── Icon-App-512x512.png │ │ │ └── Icon-App-512x512@2x.png │ │ ├── ClearDarkGray.imageset │ │ │ ├── ClearDarkGray.png │ │ │ ├── ClearDarkGray@2x.png │ │ │ ├── ClearDarkGray@3x.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Glass.imageset │ │ │ ├── Contents.json │ │ │ ├── Glass.png │ │ │ ├── Glass@2x.png │ │ │ └── Glass@3x.png │ │ ├── Icon-60.png │ │ ├── Icon-72.png │ │ ├── Icon-72@2x.png │ │ ├── Icon-Small-50.png │ │ ├── Icon-Small-50@2x.png │ │ ├── Icon.png │ │ ├── Icon@2x.png │ │ ├── ItunesArtwork.imageset │ │ │ ├── Contents.json │ │ │ ├── ItunesArtwork.png │ │ │ └── ItunesArtwork@2x.png │ │ ├── ItunesArtwork.png │ │ ├── ItunesArtwork@2x.png │ │ └── Lock.imageset │ │ │ ├── Contents.json │ │ │ ├── Lock.png │ │ │ ├── Lock@2x.png │ │ │ └── Lock@3x.png │ ├── Base.lproj │ │ └── Main.storyboard │ └── Info.plist └── Screenshots │ ├── ACBTokenFieldGif1.gif │ ├── ACBTokenFieldGif2.gif │ ├── ACBTokenFieldGif3.gif │ ├── ACBTokenFieldGif4.gif │ ├── ACBTokenFieldGif5.gif │ └── ACBTokenFieldImage1.png ├── ACBTokenFieldFramework ├── ACBTokenFieldFramework.h └── Info.plist ├── ACBTokenFieldFrameworkTests ├── ACBTokenFieldFrameworkTests.swift └── Info.plist ├── LICENSE ├── Package.swift ├── README.md └── Sources └── ACBTokenField ├── Extensions └── NSTokenField+ACBExtension.swift ├── Helpers └── ACBAssociation.swift └── Models └── ACBToken.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | 3 | ## Build generated 4 | build/ 5 | DerivedData/ 6 | 7 | ## Various settings 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata/ 17 | 18 | ## Other 19 | *.moved-aside 20 | *.xccheckout 21 | *.xcscmblueprint 22 | 23 | ## Obj-C/Swift specific 24 | *.hmap 25 | *.ipa 26 | *.dSYM.zip 27 | *.dSYM 28 | 29 | ## CocoaPods 30 | Pods/ 31 | 32 | ## Carthage 33 | Carthage/Checkouts 34 | Carthage/Build -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /ACBTokenField.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ACBTokenField" 3 | s.version = "2.3.0" 4 | s.summary = "A swift extension on NSTokenField which makes it highly customizable and removes a lot of boilerplate code from implementation." 5 | s.description = <<-DESC 6 | A swift extension on NSTokenField which makes it highly customizable and removes a lot of boilerplate code from implementation. Major features are: No need to subclass/or change anything in XIB/Storyboard. Added few properties which makes it customizable such as `shouldDisplayClearButton`, `shouldDisplaySearchIcon`, `leftView`, `shouldEnableTokenMenu` etc.. No need to implement delegate methods for simpler use cases. Just set an array of token names list or provide a default list of tokens for all indices. Rest will be handled by `NSTokenField`. Supports `NSTokenFieldDelegate` as well with the customization. Just set `tokenDelegate` and implement the methods as usual. Added support for getting `selectedTokenIndex` so that tokens can be customized based on the index. `tokenIndex` provided in `NSTokenFieldDelegate` method has a bug and hence always returns zero. `selectedTokenIndex` will help in the meantime. Support for adding tokens. Support for resetting tokens. Get `tokenIndex` based on the `representedObject` param in delegate methods. 7 | DESC 8 | s.homepage = "https://github.com/akhilcb/ACBTokenField" 9 | s.license = "MIT" 10 | s.author = "Akhil" 11 | s.platform = :osx, '10.10' 12 | s.source = { :git => "https://github.com/akhilcb/ACBTokenField.git", :tag => "2.3.0" } 13 | s.source_files = "ACBTokenField", "Sources/ACBTokenField/**/*.{swift}" 14 | s.resources = "ACBTokenField/Resources/Assets.xcassets" 15 | end 16 | -------------------------------------------------------------------------------- /ACBTokenField.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E91DF628200603D80099939B /* ACBTokenFieldFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = E91DF618200603D70099939B /* ACBTokenFieldFramework.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | E91DF6382006043B0099939B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E9C94E811F326E7600CC8573 /* Assets.xcassets */; }; 12 | E9856BA1203B8A63009EA867 /* ACBToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9856B9C203B8A63009EA867 /* ACBToken.swift */; }; 13 | E9856BA2203B8A63009EA867 /* NSTokenField+ACBExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9856B9E203B8A63009EA867 /* NSTokenField+ACBExtension.swift */; }; 14 | E9856BA3203B8A63009EA867 /* ACBAssociation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9856BA0203B8A63009EA867 /* ACBAssociation.swift */; }; 15 | E9856BA4203B8A68009EA867 /* ACBToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9856B9C203B8A63009EA867 /* ACBToken.swift */; }; 16 | E9856BA5203B8A6C009EA867 /* NSTokenField+ACBExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9856B9E203B8A63009EA867 /* NSTokenField+ACBExtension.swift */; }; 17 | E9856BA6203B8A6F009EA867 /* ACBAssociation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9856BA0203B8A63009EA867 /* ACBAssociation.swift */; }; 18 | E9C94E821F326E7600CC8573 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E9C94E811F326E7600CC8573 /* Assets.xcassets */; }; 19 | E9F93A061F2ED2020004AC10 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F93A021F2ED2020004AC10 /* ViewController.swift */; }; 20 | E9F93A071F2ED2020004AC10 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F93A051F2ED2020004AC10 /* AppDelegate.swift */; }; 21 | E9F93A0E1F2ED34F0004AC10 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E9F93A0A1F2ED34F0004AC10 /* Main.storyboard */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | E91DF616200603D70099939B /* ACBTokenField.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ACBTokenField.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | E91DF618200603D70099939B /* ACBTokenFieldFramework.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ACBTokenFieldFramework.h; sourceTree = ""; }; 27 | E91DF619200603D70099939B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 28 | E91DF625200603D80099939B /* ACBTokenFieldFrameworkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACBTokenFieldFrameworkTests.swift; sourceTree = ""; }; 29 | E91DF627200603D80099939B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 30 | E9856B9C203B8A63009EA867 /* ACBToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ACBToken.swift; sourceTree = ""; }; 31 | E9856B9E203B8A63009EA867 /* NSTokenField+ACBExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTokenField+ACBExtension.swift"; sourceTree = ""; }; 32 | E9856BA0203B8A63009EA867 /* ACBAssociation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ACBAssociation.swift; sourceTree = ""; }; 33 | E9C94E811F326E7600CC8573 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 34 | E9F939EE1F2EB1AF0004AC10 /* ACBTokenField.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ACBTokenField.app; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | E9F93A021F2ED2020004AC10 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 36 | E9F93A051F2ED2020004AC10 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | E9F93A0B1F2ED34F0004AC10 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 38 | E9F93A0C1F2ED34F0004AC10 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | E91DF612200603D70099939B /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | E9F939EB1F2EB1AF0004AC10 /* Frameworks */ = { 50 | isa = PBXFrameworksBuildPhase; 51 | buildActionMask = 2147483647; 52 | files = ( 53 | ); 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | /* End PBXFrameworksBuildPhase section */ 57 | 58 | /* Begin PBXGroup section */ 59 | E91DF617200603D70099939B /* ACBTokenFieldFramework */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | E91DF618200603D70099939B /* ACBTokenFieldFramework.h */, 63 | E91DF619200603D70099939B /* Info.plist */, 64 | ); 65 | path = ACBTokenFieldFramework; 66 | sourceTree = ""; 67 | }; 68 | E91DF624200603D80099939B /* ACBTokenFieldFrameworkTests */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | E91DF625200603D80099939B /* ACBTokenFieldFrameworkTests.swift */, 72 | E91DF627200603D80099939B /* Info.plist */, 73 | ); 74 | path = ACBTokenFieldFrameworkTests; 75 | sourceTree = ""; 76 | }; 77 | E9856B99203B8A63009EA867 /* Sources */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | E9856B9A203B8A63009EA867 /* ACBTokenField */, 81 | ); 82 | path = Sources; 83 | sourceTree = SOURCE_ROOT; 84 | }; 85 | E9856B9A203B8A63009EA867 /* ACBTokenField */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | E9856B9B203B8A63009EA867 /* Models */, 89 | E9856B9D203B8A63009EA867 /* Extensions */, 90 | E9856B9F203B8A63009EA867 /* Helpers */, 91 | ); 92 | path = ACBTokenField; 93 | sourceTree = ""; 94 | }; 95 | E9856B9B203B8A63009EA867 /* Models */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | E9856B9C203B8A63009EA867 /* ACBToken.swift */, 99 | ); 100 | path = Models; 101 | sourceTree = ""; 102 | }; 103 | E9856B9D203B8A63009EA867 /* Extensions */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | E9856B9E203B8A63009EA867 /* NSTokenField+ACBExtension.swift */, 107 | ); 108 | path = Extensions; 109 | sourceTree = ""; 110 | }; 111 | E9856B9F203B8A63009EA867 /* Helpers */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | E9856BA0203B8A63009EA867 /* ACBAssociation.swift */, 115 | ); 116 | path = Helpers; 117 | sourceTree = ""; 118 | }; 119 | E9F939E51F2EB1AF0004AC10 = { 120 | isa = PBXGroup; 121 | children = ( 122 | E9F939F01F2EB1AF0004AC10 /* ACBTokenField */, 123 | E9F93A081F2ED34F0004AC10 /* Resources */, 124 | E91DF617200603D70099939B /* ACBTokenFieldFramework */, 125 | E91DF624200603D80099939B /* ACBTokenFieldFrameworkTests */, 126 | E9F939EF1F2EB1AF0004AC10 /* Products */, 127 | ); 128 | sourceTree = ""; 129 | }; 130 | E9F939EF1F2EB1AF0004AC10 /* Products */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | E9F939EE1F2EB1AF0004AC10 /* ACBTokenField.app */, 134 | E91DF616200603D70099939B /* ACBTokenField.framework */, 135 | ); 136 | name = Products; 137 | sourceTree = ""; 138 | }; 139 | E9F939F01F2EB1AF0004AC10 /* ACBTokenField */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | E9856B99203B8A63009EA867 /* Sources */, 143 | E9F93A041F2ED2020004AC10 /* General */, 144 | E9F93A011F2ED2020004AC10 /* ViewControllers */, 145 | ); 146 | path = ACBTokenField; 147 | sourceTree = ""; 148 | }; 149 | E9F93A011F2ED2020004AC10 /* ViewControllers */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | E9F93A021F2ED2020004AC10 /* ViewController.swift */, 153 | ); 154 | name = ViewControllers; 155 | path = Classes/ViewControllers; 156 | sourceTree = ""; 157 | }; 158 | E9F93A041F2ED2020004AC10 /* General */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | E9F93A051F2ED2020004AC10 /* AppDelegate.swift */, 162 | ); 163 | name = General; 164 | path = Classes/General; 165 | sourceTree = ""; 166 | }; 167 | E9F93A081F2ED34F0004AC10 /* Resources */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | E9C94E811F326E7600CC8573 /* Assets.xcassets */, 171 | E9F93A0A1F2ED34F0004AC10 /* Main.storyboard */, 172 | E9F93A0C1F2ED34F0004AC10 /* Info.plist */, 173 | ); 174 | name = Resources; 175 | path = ACBTokenField/Resources; 176 | sourceTree = ""; 177 | }; 178 | /* End PBXGroup section */ 179 | 180 | /* Begin PBXHeadersBuildPhase section */ 181 | E91DF613200603D70099939B /* Headers */ = { 182 | isa = PBXHeadersBuildPhase; 183 | buildActionMask = 2147483647; 184 | files = ( 185 | E91DF628200603D80099939B /* ACBTokenFieldFramework.h in Headers */, 186 | ); 187 | runOnlyForDeploymentPostprocessing = 0; 188 | }; 189 | /* End PBXHeadersBuildPhase section */ 190 | 191 | /* Begin PBXNativeTarget section */ 192 | E91DF615200603D70099939B /* ACBTokenFieldFramework */ = { 193 | isa = PBXNativeTarget; 194 | buildConfigurationList = E91DF631200603D80099939B /* Build configuration list for PBXNativeTarget "ACBTokenFieldFramework" */; 195 | buildPhases = ( 196 | E91DF611200603D70099939B /* Sources */, 197 | E91DF612200603D70099939B /* Frameworks */, 198 | E91DF613200603D70099939B /* Headers */, 199 | E91DF614200603D70099939B /* Resources */, 200 | ); 201 | buildRules = ( 202 | ); 203 | dependencies = ( 204 | ); 205 | name = ACBTokenFieldFramework; 206 | productName = ACBTokenFieldFramework; 207 | productReference = E91DF616200603D70099939B /* ACBTokenField.framework */; 208 | productType = "com.apple.product-type.framework"; 209 | }; 210 | E9F939ED1F2EB1AF0004AC10 /* ACBTokenField */ = { 211 | isa = PBXNativeTarget; 212 | buildConfigurationList = E9F939FD1F2EB1AF0004AC10 /* Build configuration list for PBXNativeTarget "ACBTokenField" */; 213 | buildPhases = ( 214 | E9F939EA1F2EB1AF0004AC10 /* Sources */, 215 | E9F939EB1F2EB1AF0004AC10 /* Frameworks */, 216 | E9F939EC1F2EB1AF0004AC10 /* Resources */, 217 | ); 218 | buildRules = ( 219 | ); 220 | dependencies = ( 221 | ); 222 | name = ACBTokenField; 223 | productName = ACBTokenField; 224 | productReference = E9F939EE1F2EB1AF0004AC10 /* ACBTokenField.app */; 225 | productType = "com.apple.product-type.application"; 226 | }; 227 | /* End PBXNativeTarget section */ 228 | 229 | /* Begin PBXProject section */ 230 | E9F939E61F2EB1AF0004AC10 /* Project object */ = { 231 | isa = PBXProject; 232 | attributes = { 233 | BuildIndependentTargetsInParallel = YES; 234 | LastSwiftUpdateCheck = 0920; 235 | LastUpgradeCheck = 1540; 236 | ORGANIZATIONNAME = akhil; 237 | TargetAttributes = { 238 | E91DF615200603D70099939B = { 239 | CreatedOnToolsVersion = 9.2; 240 | LastSwiftMigration = 1540; 241 | ProvisioningStyle = Automatic; 242 | }; 243 | E9F939ED1F2EB1AF0004AC10 = { 244 | CreatedOnToolsVersion = 8.3.3; 245 | LastSwiftMigration = 1540; 246 | ProvisioningStyle = Automatic; 247 | }; 248 | }; 249 | }; 250 | buildConfigurationList = E9F939E91F2EB1AF0004AC10 /* Build configuration list for PBXProject "ACBTokenField" */; 251 | compatibilityVersion = "Xcode 3.2"; 252 | developmentRegion = en; 253 | hasScannedForEncodings = 0; 254 | knownRegions = ( 255 | en, 256 | Base, 257 | ); 258 | mainGroup = E9F939E51F2EB1AF0004AC10; 259 | productRefGroup = E9F939EF1F2EB1AF0004AC10 /* Products */; 260 | projectDirPath = ""; 261 | projectRoot = ""; 262 | targets = ( 263 | E9F939ED1F2EB1AF0004AC10 /* ACBTokenField */, 264 | E91DF615200603D70099939B /* ACBTokenFieldFramework */, 265 | ); 266 | }; 267 | /* End PBXProject section */ 268 | 269 | /* Begin PBXResourcesBuildPhase section */ 270 | E91DF614200603D70099939B /* Resources */ = { 271 | isa = PBXResourcesBuildPhase; 272 | buildActionMask = 2147483647; 273 | files = ( 274 | E91DF6382006043B0099939B /* Assets.xcassets in Resources */, 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | }; 278 | E9F939EC1F2EB1AF0004AC10 /* Resources */ = { 279 | isa = PBXResourcesBuildPhase; 280 | buildActionMask = 2147483647; 281 | files = ( 282 | E9C94E821F326E7600CC8573 /* Assets.xcassets in Resources */, 283 | E9F93A0E1F2ED34F0004AC10 /* Main.storyboard in Resources */, 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | /* End PBXResourcesBuildPhase section */ 288 | 289 | /* Begin PBXSourcesBuildPhase section */ 290 | E91DF611200603D70099939B /* Sources */ = { 291 | isa = PBXSourcesBuildPhase; 292 | buildActionMask = 2147483647; 293 | files = ( 294 | E9856BA5203B8A6C009EA867 /* NSTokenField+ACBExtension.swift in Sources */, 295 | E9856BA6203B8A6F009EA867 /* ACBAssociation.swift in Sources */, 296 | E9856BA4203B8A68009EA867 /* ACBToken.swift in Sources */, 297 | ); 298 | runOnlyForDeploymentPostprocessing = 0; 299 | }; 300 | E9F939EA1F2EB1AF0004AC10 /* Sources */ = { 301 | isa = PBXSourcesBuildPhase; 302 | buildActionMask = 2147483647; 303 | files = ( 304 | E9F93A071F2ED2020004AC10 /* AppDelegate.swift in Sources */, 305 | E9F93A061F2ED2020004AC10 /* ViewController.swift in Sources */, 306 | E9856BA2203B8A63009EA867 /* NSTokenField+ACBExtension.swift in Sources */, 307 | E9856BA3203B8A63009EA867 /* ACBAssociation.swift in Sources */, 308 | E9856BA1203B8A63009EA867 /* ACBToken.swift in Sources */, 309 | ); 310 | runOnlyForDeploymentPostprocessing = 0; 311 | }; 312 | /* End PBXSourcesBuildPhase section */ 313 | 314 | /* Begin PBXVariantGroup section */ 315 | E9F93A0A1F2ED34F0004AC10 /* Main.storyboard */ = { 316 | isa = PBXVariantGroup; 317 | children = ( 318 | E9F93A0B1F2ED34F0004AC10 /* Base */, 319 | ); 320 | name = Main.storyboard; 321 | sourceTree = ""; 322 | }; 323 | /* End PBXVariantGroup section */ 324 | 325 | /* Begin XCBuildConfiguration section */ 326 | E91DF62D200603D80099939B /* Debug */ = { 327 | isa = XCBuildConfiguration; 328 | buildSettings = { 329 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 330 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 331 | CLANG_WARN_COMMA = YES; 332 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 333 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 334 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 335 | CLANG_WARN_STRICT_PROTOTYPES = YES; 336 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 337 | CODE_SIGN_IDENTITY = ""; 338 | CODE_SIGN_STYLE = Automatic; 339 | COMBINE_HIDPI_IMAGES = YES; 340 | CURRENT_PROJECT_VERSION = 1; 341 | DEAD_CODE_STRIPPING = YES; 342 | DEFINES_MODULE = YES; 343 | DYLIB_COMPATIBILITY_VERSION = 1; 344 | DYLIB_CURRENT_VERSION = 1; 345 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 346 | ENABLE_MODULE_VERIFIER = YES; 347 | FRAMEWORK_VERSION = A; 348 | GCC_C_LANGUAGE_STANDARD = gnu11; 349 | INFOPLIST_FILE = ACBTokenFieldFramework/Info.plist; 350 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 351 | LD_RUNPATH_SEARCH_PATHS = ( 352 | "$(inherited)", 353 | "@executable_path/../Frameworks", 354 | "@loader_path/Frameworks", 355 | ); 356 | MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; 357 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; 358 | PRODUCT_BUNDLE_IDENTIFIER = com.akhil.ACBTokenFieldFramework; 359 | PRODUCT_NAME = ACBTokenField; 360 | SKIP_INSTALL = YES; 361 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 362 | SWIFT_VERSION = 5.0; 363 | VERSIONING_SYSTEM = "apple-generic"; 364 | VERSION_INFO_PREFIX = ""; 365 | }; 366 | name = Debug; 367 | }; 368 | E91DF62E200603D80099939B /* Release */ = { 369 | isa = XCBuildConfiguration; 370 | buildSettings = { 371 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 372 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 373 | CLANG_WARN_COMMA = YES; 374 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 375 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 376 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 377 | CLANG_WARN_STRICT_PROTOTYPES = YES; 378 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 379 | CODE_SIGN_IDENTITY = ""; 380 | CODE_SIGN_STYLE = Automatic; 381 | COMBINE_HIDPI_IMAGES = YES; 382 | CURRENT_PROJECT_VERSION = 1; 383 | DEAD_CODE_STRIPPING = YES; 384 | DEFINES_MODULE = YES; 385 | DYLIB_COMPATIBILITY_VERSION = 1; 386 | DYLIB_CURRENT_VERSION = 1; 387 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 388 | ENABLE_MODULE_VERIFIER = YES; 389 | FRAMEWORK_VERSION = A; 390 | GCC_C_LANGUAGE_STANDARD = gnu11; 391 | INFOPLIST_FILE = ACBTokenFieldFramework/Info.plist; 392 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 393 | LD_RUNPATH_SEARCH_PATHS = ( 394 | "$(inherited)", 395 | "@executable_path/../Frameworks", 396 | "@loader_path/Frameworks", 397 | ); 398 | MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; 399 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; 400 | PRODUCT_BUNDLE_IDENTIFIER = com.akhil.ACBTokenFieldFramework; 401 | PRODUCT_NAME = ACBTokenField; 402 | SKIP_INSTALL = YES; 403 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 404 | SWIFT_VERSION = 5.0; 405 | VERSIONING_SYSTEM = "apple-generic"; 406 | VERSION_INFO_PREFIX = ""; 407 | }; 408 | name = Release; 409 | }; 410 | E9F939FB1F2EB1AF0004AC10 /* Debug */ = { 411 | isa = XCBuildConfiguration; 412 | buildSettings = { 413 | ALWAYS_SEARCH_USER_PATHS = NO; 414 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 415 | CLANG_ANALYZER_NONNULL = YES; 416 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 417 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 418 | CLANG_CXX_LIBRARY = "libc++"; 419 | CLANG_ENABLE_MODULES = YES; 420 | CLANG_ENABLE_OBJC_ARC = YES; 421 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 422 | CLANG_WARN_BOOL_CONVERSION = YES; 423 | CLANG_WARN_COMMA = YES; 424 | CLANG_WARN_CONSTANT_CONVERSION = YES; 425 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 426 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 427 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 428 | CLANG_WARN_EMPTY_BODY = YES; 429 | CLANG_WARN_ENUM_CONVERSION = YES; 430 | CLANG_WARN_INFINITE_RECURSION = YES; 431 | CLANG_WARN_INT_CONVERSION = YES; 432 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 433 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 434 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 435 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 436 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 437 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 438 | CLANG_WARN_STRICT_PROTOTYPES = YES; 439 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 440 | CLANG_WARN_UNREACHABLE_CODE = YES; 441 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 442 | CODE_SIGN_IDENTITY = "-"; 443 | COPY_PHASE_STRIP = NO; 444 | DEAD_CODE_STRIPPING = YES; 445 | DEBUG_INFORMATION_FORMAT = dwarf; 446 | ENABLE_STRICT_OBJC_MSGSEND = YES; 447 | ENABLE_TESTABILITY = YES; 448 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 449 | GCC_C_LANGUAGE_STANDARD = gnu99; 450 | GCC_DYNAMIC_NO_PIC = NO; 451 | GCC_NO_COMMON_BLOCKS = YES; 452 | GCC_OPTIMIZATION_LEVEL = 0; 453 | GCC_PREPROCESSOR_DEFINITIONS = ( 454 | "DEBUG=1", 455 | "$(inherited)", 456 | ); 457 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 458 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 459 | GCC_WARN_UNDECLARED_SELECTOR = YES; 460 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 461 | GCC_WARN_UNUSED_FUNCTION = YES; 462 | GCC_WARN_UNUSED_VARIABLE = YES; 463 | MACOSX_DEPLOYMENT_TARGET = 10.10; 464 | MTL_ENABLE_DEBUG_INFO = YES; 465 | ONLY_ACTIVE_ARCH = YES; 466 | SDKROOT = macosx; 467 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 468 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 469 | SWIFT_VERSION = 4.2; 470 | }; 471 | name = Debug; 472 | }; 473 | E9F939FC1F2EB1AF0004AC10 /* Release */ = { 474 | isa = XCBuildConfiguration; 475 | buildSettings = { 476 | ALWAYS_SEARCH_USER_PATHS = NO; 477 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 478 | CLANG_ANALYZER_NONNULL = YES; 479 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 480 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 481 | CLANG_CXX_LIBRARY = "libc++"; 482 | CLANG_ENABLE_MODULES = YES; 483 | CLANG_ENABLE_OBJC_ARC = YES; 484 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 485 | CLANG_WARN_BOOL_CONVERSION = YES; 486 | CLANG_WARN_COMMA = YES; 487 | CLANG_WARN_CONSTANT_CONVERSION = YES; 488 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 489 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 490 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 491 | CLANG_WARN_EMPTY_BODY = YES; 492 | CLANG_WARN_ENUM_CONVERSION = YES; 493 | CLANG_WARN_INFINITE_RECURSION = YES; 494 | CLANG_WARN_INT_CONVERSION = YES; 495 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 496 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 497 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 498 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 499 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 500 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 501 | CLANG_WARN_STRICT_PROTOTYPES = YES; 502 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 503 | CLANG_WARN_UNREACHABLE_CODE = YES; 504 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 505 | CODE_SIGN_IDENTITY = "-"; 506 | COPY_PHASE_STRIP = NO; 507 | DEAD_CODE_STRIPPING = YES; 508 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 509 | ENABLE_NS_ASSERTIONS = NO; 510 | ENABLE_STRICT_OBJC_MSGSEND = YES; 511 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 512 | GCC_C_LANGUAGE_STANDARD = gnu99; 513 | GCC_NO_COMMON_BLOCKS = YES; 514 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 515 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 516 | GCC_WARN_UNDECLARED_SELECTOR = YES; 517 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 518 | GCC_WARN_UNUSED_FUNCTION = YES; 519 | GCC_WARN_UNUSED_VARIABLE = YES; 520 | MACOSX_DEPLOYMENT_TARGET = 10.10; 521 | MTL_ENABLE_DEBUG_INFO = NO; 522 | SDKROOT = macosx; 523 | SWIFT_COMPILATION_MODE = wholemodule; 524 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 525 | SWIFT_VERSION = 4.2; 526 | }; 527 | name = Release; 528 | }; 529 | E9F939FE1F2EB1AF0004AC10 /* Debug */ = { 530 | isa = XCBuildConfiguration; 531 | buildSettings = { 532 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 533 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 534 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 535 | COMBINE_HIDPI_IMAGES = YES; 536 | DEAD_CODE_STRIPPING = YES; 537 | INFOPLIST_FILE = "$(SRCROOT)/ACBTokenField/Resources/Info.plist"; 538 | LD_RUNPATH_SEARCH_PATHS = ( 539 | "$(inherited)", 540 | "@executable_path/../Frameworks", 541 | ); 542 | MACOSX_DEPLOYMENT_TARGET = 11.0; 543 | PRODUCT_BUNDLE_IDENTIFIER = com.akhil.ACBTokenField; 544 | PRODUCT_NAME = "$(TARGET_NAME)"; 545 | SWIFT_VERSION = 5.0; 546 | }; 547 | name = Debug; 548 | }; 549 | E9F939FF1F2EB1AF0004AC10 /* Release */ = { 550 | isa = XCBuildConfiguration; 551 | buildSettings = { 552 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 553 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 554 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 555 | COMBINE_HIDPI_IMAGES = YES; 556 | DEAD_CODE_STRIPPING = YES; 557 | INFOPLIST_FILE = "$(SRCROOT)/ACBTokenField/Resources/Info.plist"; 558 | LD_RUNPATH_SEARCH_PATHS = ( 559 | "$(inherited)", 560 | "@executable_path/../Frameworks", 561 | ); 562 | MACOSX_DEPLOYMENT_TARGET = 11.0; 563 | PRODUCT_BUNDLE_IDENTIFIER = com.akhil.ACBTokenField; 564 | PRODUCT_NAME = "$(TARGET_NAME)"; 565 | SWIFT_VERSION = 5.0; 566 | }; 567 | name = Release; 568 | }; 569 | /* End XCBuildConfiguration section */ 570 | 571 | /* Begin XCConfigurationList section */ 572 | E91DF631200603D80099939B /* Build configuration list for PBXNativeTarget "ACBTokenFieldFramework" */ = { 573 | isa = XCConfigurationList; 574 | buildConfigurations = ( 575 | E91DF62D200603D80099939B /* Debug */, 576 | E91DF62E200603D80099939B /* Release */, 577 | ); 578 | defaultConfigurationIsVisible = 0; 579 | defaultConfigurationName = Release; 580 | }; 581 | E9F939E91F2EB1AF0004AC10 /* Build configuration list for PBXProject "ACBTokenField" */ = { 582 | isa = XCConfigurationList; 583 | buildConfigurations = ( 584 | E9F939FB1F2EB1AF0004AC10 /* Debug */, 585 | E9F939FC1F2EB1AF0004AC10 /* Release */, 586 | ); 587 | defaultConfigurationIsVisible = 0; 588 | defaultConfigurationName = Release; 589 | }; 590 | E9F939FD1F2EB1AF0004AC10 /* Build configuration list for PBXNativeTarget "ACBTokenField" */ = { 591 | isa = XCConfigurationList; 592 | buildConfigurations = ( 593 | E9F939FE1F2EB1AF0004AC10 /* Debug */, 594 | E9F939FF1F2EB1AF0004AC10 /* Release */, 595 | ); 596 | defaultConfigurationIsVisible = 0; 597 | defaultConfigurationName = Release; 598 | }; 599 | /* End XCConfigurationList section */ 600 | }; 601 | rootObject = E9F939E61F2EB1AF0004AC10 /* Project object */; 602 | } 603 | -------------------------------------------------------------------------------- /ACBTokenField.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ACBTokenField.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ACBTokenField.xcodeproj/xcshareddata/xcschemes/ACBTokenField.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /ACBTokenField.xcodeproj/xcshareddata/xcschemes/ACBTokenFieldFramework.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /ACBTokenField/Classes/General/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ACBTokenField 4 | // 5 | // Created by Akhil on 7/30/17. 6 | // Copyright © 2017 akhil. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | func applicationDidFinishLaunching(_ aNotification: Notification) { 15 | // Insert code here to initialize your application 16 | } 17 | 18 | func applicationWillTerminate(_ aNotification: Notification) { 19 | // Insert code here to tear down your application 20 | } 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /ACBTokenField/Classes/ViewControllers/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ACBTokenField 4 | // 5 | // Created by Akhil on 7/30/17. 6 | // Copyright © 2017 akhil. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ViewController: NSViewController { 12 | 13 | @IBOutlet var tokenFieldRandom: NSTokenField! 14 | @IBOutlet var tokenFieldMovie: NSTokenField! 15 | @IBOutlet weak var enableClearButton: NSButton! 16 | @IBOutlet weak var enableSearchIcon: NSButton! 17 | @IBOutlet weak var setCustomLeftView: NSButton! 18 | @IBOutlet weak var enableTokenMenu: NSButton! 19 | @IBOutlet var textView: NSTextView! 20 | @IBOutlet var textViewMovie: NSTextView! 21 | 22 | let actors = ["Ryan Gosling", "Ryan Reynolds", "Jeremy Renner", "Chris Pratt", "Bradley Cooper"] 23 | let actresses = ["Emma Stone", "Blake Lively", "Amy Adams", "Jennifer Lawrence"] 24 | let movies = ["La La Land", "Green Lantern", "Arrival", "Passengers", "Silver Linings Playbook"] 25 | let years = ["2011", "2012", "2013", "2014", "2016", "2017"] 26 | 27 | var actorsMenu = NSMenu() 28 | var actressMenu = NSMenu() 29 | var moviesMenu = NSMenu() 30 | var yearsMenu = NSMenu() 31 | 32 | lazy var imageView: NSImageView = { 33 | if #available(OSX 10.12, *) { 34 | return NSImageView(image: NSImage(named: "Lock")!) 35 | } else { 36 | // Fallback on earlier versions 37 | let imageView = NSImageView() 38 | imageView.image = NSImage(named: "Lock")! 39 | return imageView 40 | } 41 | }() 42 | 43 | 44 | // #MARK: Override 45 | 46 | override func viewDidLoad() { 47 | super.viewDidLoad() 48 | 49 | setupViews() 50 | var button: NSButton! 51 | if #available(OSX 10.12, *) { 52 | button = NSButton(image: NSImage(named: "Lock")!, 53 | target: self, 54 | action: #selector(self.lockButtonTapped(_:))) 55 | } else { 56 | // Fallback on earlier versions 57 | button = NSButton() 58 | button.image = NSImage(named: "Lock")! 59 | button.target = self 60 | button.action = #selector(self.lockButtonTapped(_:)) 61 | } 62 | button.setButtonType(.momentaryChange) 63 | button.isBordered = false 64 | 65 | tokenFieldRandom.placeholderString = "Enter tokens in following order: Country Food Animal Vehicle Color(this can repeat)" 66 | 67 | //convert tokenField to ACBTokenField 68 | tokenFieldRandom.convertToACBTokenField() 69 | 70 | //set any required properties 71 | tokenFieldRandom.shouldEnableTokenMenu = true 72 | tokenFieldRandom.tokenKeywordsList = [["France", "Germany", "Italy", "USA", "Spain", "India", "Brazil"], 73 | ["Pizza", "Pasta", "Butter Chicken", "Jamon", "Cheesecake"], 74 | ["Deer", "Dog", "Bear", "Panda", "Jaguar", "Bull"], 75 | ["Car", "Truck", "Bus", "Motorcycle", "Minivan"]] 76 | 77 | tokenFieldRandom.defaultTokenKeywords = ["Red", "Blue", "Green", "White", "Purple", "Black"] 78 | tokenFieldRandom.tokenDelegate = self 79 | tokenFieldRandom.shouldDisplaySearchIcon = true 80 | 81 | //convert tokenField to ACBTokenField 82 | tokenFieldMovie.convertToACBTokenField() 83 | 84 | //set any required properties 85 | tokenFieldMovie.shouldEnableTokenMenu = true 86 | tokenFieldMovie.tokenDelegate = self 87 | tokenFieldMovie.leftView = button 88 | 89 | tokenFieldRandom.didDeleteTokenBlock = { (tokenIndex, _) in 90 | print("Random: Token at index = ", tokenIndex, "is removed") 91 | } 92 | 93 | tokenFieldMovie.didDeleteTokenBlock = { (tokenIndex, _) in 94 | print("Movie: Token at index = ", tokenIndex, "is removed") 95 | } 96 | } 97 | 98 | 99 | // #MARK: Private 100 | 101 | private func setupViews() { 102 | setupMenu(actorsMenu, nameList: actors) 103 | setupMenu(actressMenu, nameList: actresses) 104 | setupMenu(moviesMenu, nameList: movies) 105 | setupMenu(yearsMenu, nameList: years) 106 | 107 | enableTokenMenu.state = .on 108 | enableSearchIcon.state = .on 109 | setCustomLeftView.state = .off 110 | enableTokenMenu.state = .on 111 | } 112 | 113 | private func setupMenu(_ menu: NSMenu, nameList: [String]) { 114 | nameList.forEach { 115 | menu.addItem(withTitle: $0, 116 | action: #selector(menuItemTapped(_:)), 117 | keyEquivalent: "").target = self 118 | } 119 | } 120 | 121 | @objc private func lockButtonTapped(_ sender: Any?) { 122 | tokenFieldMovie.isEnabled = !tokenFieldMovie.isEnabled 123 | 124 | if !tokenFieldMovie.isEnabled { 125 | textViewMovie.string = "TokenField disabled! Tap again on Lock icon to enable" 126 | } else { 127 | textViewMovie.string = "TokenField enabled!" 128 | } 129 | } 130 | 131 | @objc private func menuItemTapped(_ menuItem: NSMenuItem) { 132 | if let fieldEditor = tokenFieldMovie?.currentEditor() { 133 | let textRange = fieldEditor.selectedRange 134 | let replaceString = menuItem.title 135 | fieldEditor.replaceCharacters(in: textRange, with: replaceString) 136 | fieldEditor.selectedRange = NSMakeRange(textRange.location, replaceString.count) 137 | } 138 | } 139 | 140 | 141 | // #MARK: IBAction 142 | 143 | @IBAction func enableClearButton(_ sender: NSButton) { 144 | tokenFieldRandom.shouldDisplayClearButton = sender.state == .on 145 | } 146 | 147 | @IBAction func enableSearchIcon(_ sender: NSButton) { 148 | if sender.state == .on { 149 | setCustomLeftView.state = .off 150 | tokenFieldRandom.leftView = nil 151 | } 152 | tokenFieldRandom.shouldDisplaySearchIcon = sender.state == .on 153 | } 154 | 155 | @IBAction func setLeftView(_ sender: NSButton) { 156 | if sender.state == .on { 157 | tokenFieldRandom.leftView = imageView 158 | enableSearchIcon.state = .off 159 | } else { 160 | tokenFieldRandom.leftView = nil 161 | } 162 | } 163 | 164 | @IBAction func enableTokenMenu(_ sender: NSButton) { 165 | tokenFieldRandom.shouldEnableTokenMenu = sender.state == .on 166 | tokenFieldRandom.resetTokens() 167 | } 168 | 169 | @IBAction func resultTapped(_ sender: Any) { 170 | textView.string = tokenFieldRandom.tokenStringValue 171 | } 172 | 173 | @IBAction func resultMovieTapped(_ sender: Any) { 174 | textViewMovie.string = tokenFieldMovie.tokenStringValue 175 | } 176 | 177 | } 178 | 179 | 180 | extension ViewController: NSTokenFieldDelegate { 181 | 182 | public func tokenField(_ tokenField: NSTokenField, styleForRepresentedObject representedObject: Any) -> NSTokenField.TokenStyle { 183 | return .rounded 184 | } 185 | 186 | public func tokenField(_ tokenField: NSTokenField, completionsForSubstring substring: String, indexOfToken tokenIndex: Int, indexOfSelectedItem selectedIndex: UnsafeMutablePointer?) -> [Any]? { 187 | 188 | switch tokenIndex { 189 | case 0: 190 | return actors 191 | case 1: 192 | return actresses 193 | case 2: 194 | return movies 195 | case 3: 196 | return years 197 | default: 198 | return nil 199 | } 200 | } 201 | 202 | public func tokenField(_ tokenField: NSTokenField, hasMenuForRepresentedObject representedObject: Any) -> Bool { 203 | if let tokenIndex = tokenField.tokenIndex(forRepresentedObject: representedObject), 204 | (0..<5).contains(tokenIndex) { 205 | 206 | return true 207 | } 208 | 209 | return false 210 | } 211 | 212 | public func tokenField(_ tokenField: NSTokenField, menuForRepresentedObject representedObject: Any) -> NSMenu? { 213 | if let tokenIndex = tokenField.tokenIndex(forRepresentedObject: representedObject), 214 | (0..<5).contains(tokenIndex) { 215 | 216 | switch tokenIndex { 217 | case 0: 218 | return actorsMenu 219 | case 1: 220 | return actressMenu 221 | case 2: 222 | return moviesMenu 223 | case 3: 224 | return yearsMenu 225 | default: 226 | return nil 227 | } 228 | } 229 | 230 | return nil 231 | } 232 | } 233 | 234 | -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "Icon-App-16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "Icon-App-16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "Icon-App-32x32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "Icon-App-32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "Icon-App-128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "Icon-App-128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "Icon-App-256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "Icon-App-256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "Icon-App-512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "Icon-App-512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-128x128.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-128x128@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-16x16.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-16x16@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-256x256.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-256x256@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-32x32.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-32x32@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-512x512.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-512x512@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/ClearDarkGray.imageset/ClearDarkGray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/ClearDarkGray.imageset/ClearDarkGray.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/ClearDarkGray.imageset/ClearDarkGray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/ClearDarkGray.imageset/ClearDarkGray@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/ClearDarkGray.imageset/ClearDarkGray@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/ClearDarkGray.imageset/ClearDarkGray@3x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/ClearDarkGray.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ClearDarkGray.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ClearDarkGray@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ClearDarkGray@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Glass.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Glass.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Glass@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Glass@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Glass.imageset/Glass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Glass.imageset/Glass.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Glass.imageset/Glass@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Glass.imageset/Glass@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Glass.imageset/Glass@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Glass.imageset/Glass@3x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Icon-60.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Icon-72.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Icon-72@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Icon-Small-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Icon-Small-50.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Icon-Small-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Icon-Small-50@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Icon.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Icon@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/ItunesArtwork.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ItunesArtwork.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ItunesArtwork@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/ItunesArtwork.imageset/ItunesArtwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/ItunesArtwork.imageset/ItunesArtwork.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/ItunesArtwork.imageset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/ItunesArtwork.imageset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/ItunesArtwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/ItunesArtwork.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Lock.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Lock.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Lock@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Lock@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Lock.imageset/Lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Lock.imageset/Lock.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Lock.imageset/Lock@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Lock.imageset/Lock@2x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Assets.xcassets/Lock.imageset/Lock@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Resources/Assets.xcassets/Lock.imageset/Lock@3x.png -------------------------------------------------------------------------------- /ACBTokenField/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 2.3.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2018 Akhil Choran Balan. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /ACBTokenField/Screenshots/ACBTokenFieldGif1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Screenshots/ACBTokenFieldGif1.gif -------------------------------------------------------------------------------- /ACBTokenField/Screenshots/ACBTokenFieldGif2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Screenshots/ACBTokenFieldGif2.gif -------------------------------------------------------------------------------- /ACBTokenField/Screenshots/ACBTokenFieldGif3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Screenshots/ACBTokenFieldGif3.gif -------------------------------------------------------------------------------- /ACBTokenField/Screenshots/ACBTokenFieldGif4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Screenshots/ACBTokenFieldGif4.gif -------------------------------------------------------------------------------- /ACBTokenField/Screenshots/ACBTokenFieldGif5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Screenshots/ACBTokenFieldGif5.gif -------------------------------------------------------------------------------- /ACBTokenField/Screenshots/ACBTokenFieldImage1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akhilcb/ACBTokenField/65517b61c931535cbef0be3958d9e24d1b43eab2/ACBTokenField/Screenshots/ACBTokenFieldImage1.png -------------------------------------------------------------------------------- /ACBTokenFieldFramework/ACBTokenFieldFramework.h: -------------------------------------------------------------------------------- 1 | // 2 | // ACBTokenFieldFramework.h 3 | // ACBTokenFieldFramework 4 | // 5 | // Created by Akhil C Balan on 1/10/18. 6 | // Copyright © 2018 akhil. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ACBTokenFieldFramework. 12 | FOUNDATION_EXPORT double ACBTokenFieldFrameworkVersionNumber; 13 | 14 | //! Project version string for ACBTokenFieldFramework. 15 | FOUNDATION_EXPORT const unsigned char ACBTokenFieldFrameworkVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /ACBTokenFieldFramework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 2.3.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2018 Akhil Choran Balan. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ACBTokenFieldFrameworkTests/ACBTokenFieldFrameworkTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACBTokenFieldFrameworkTests.swift 3 | // ACBTokenFieldFrameworkTests 4 | // 5 | // Created by Akhil C Balan on 1/10/18. 6 | // Copyright © 2018 akhil. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ACBTokenFieldFramework 11 | 12 | class ACBTokenFieldFrameworkTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ACBTokenFieldFrameworkTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017, Akhil C Balan(https://github.com/akhilcb) 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ACBTokenField", 7 | products: [ 8 | .library( 9 | name: "ACBTokenField", 10 | targets: ["ACBTokenField"]), 11 | ], 12 | dependencies: [], 13 | targets: [ 14 | .target( 15 | name: "ACBTokenField", 16 | dependencies: []) 17 | ], 18 | swiftLanguageVersions: [5] 19 | ) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ACBTokenField 2 | 3 | [![Swift 3.2](https://img.shields.io/badge/Swift-3.2-orange.svg)](https://github.com/akhilcb/ACBTokenField) 4 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/ACBTokenField.svg)](https://img.shields.io/cocoapods/v/ACBTokenField.svg) 5 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | [![Swift Package Manager](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager) 7 | [![Platform](https://img.shields.io/cocoapods/p/ACBTokenField.svg?style=flat)](https://github.com/akhilcb/ACBTokenField) 8 | [![License](https://img.shields.io/cocoapods/l/ACBTokenField.svg?style=flat)](http://cocoapods.org/pods/ACBTokenField) 9 | 10 | 11 | A swift extension on NSTokenField which makes it highly customizable and removes a lot of boilerplate code from its implementation. 12 | 13 | ## Features 14 | 15 | - [x] Extension on NSTokenField, no need to subclass/or change anything in XIB/Storyboard 16 | - [x] Added few properties which makes it customizable such as `shouldDisplayClearButton`, `shouldDisplaySearchIcon`, `leftView`, `shouldEnableTokenMenu` etc.. 17 | - [x] No need to implement delegate methods for simpler use cases. Just set an array of token names list or provide a default list of tokens for all indices. Rest will be handled by `NSTokenField`. See demo provided below(1 - 3). 18 | - [x] Supports `NSTokenFieldDelegate` as well with the customization. Just set `tokenDelegate` and implement the methods(see gif4) as usual. 19 | - [x] Added support for getting `selectedTokenIndex` so that tokens can be customized based on the index. `tokenIndex` provided in `NSTokenFieldDelegate` method has a bug and hence always returns zero. `selectedTokenIndex` will help in the meantime. 20 | - [x] Support for adding tokens 21 | - [x] Support for resetting tokens 22 | - [x] Support for delete token callback 23 | - [x] Support for getting actual token count 24 | - [x] Get `tokenIndex` based on the `representedObject` param in delegate methods. 25 | 26 | ## Demo 27 | 28 |
29 |

30 |
31 |

32 |
33 |

34 |
35 |

36 |
37 | 38 |

39 | 40 | ## Setup 41 | 42 | Carthage, Cocoapods or Swift Package Manager can be used to integrate this to a project. 43 | 44 | ### Carthage 45 | 46 | ``` 47 | github "akhilcb/ACBTokenField" ~> 2.3.0 48 | 49 | ``` 50 | 51 | ### Cocoapods 52 | 53 | ``` 54 | pod 'ACBTokenField', '~> 2.3.0' 55 | 56 | ``` 57 | 58 | ### Swift Package Manager 59 | 60 | ``` 61 | dependencies: [ 62 | .Package(url: "https://github.com/akhilcb/ACBTokenField.git", majorVersion: 2) 63 | ] 64 | ``` 65 | 66 | ## Quick start 67 | Inorder to implement this in a project just copy the files `NSTokenField+ACBExtension.swift`, `ACBAssociation.swift`, `ACBToken.swift`and invoke the following function on any `NSTokenField`. As mentioned above, no need to subclass or change anything in XIB or Storyboard file 68 | 69 | tokenField.convertToACBTokenField() 70 | 71 | If you would like to have clear button and/or search icon in tokenfield, you can copy the icons `ClearDarkGray` and `Glass` from Assets.xcassets. Otherwise you can use your own images and set the properties `clearIconName` and `searchIconName` of `NSTokenField`. You are good to go. 72 | 73 | _Note: Cell class will be dynamically changed to `ACBTokenFieldCell` and `delegate` will be set. Please do not modify these properties. Use `tokenDelegate` instead of `delegate`._ 74 | 75 | ## Setup 76 | 77 | There are additional features which you can make use of such as setting an Array of token names list which represents token list at each index of token field. You don't have to implement the NSTokenField delegate in this case. This will take care of displaying suggestions/displaying menu on tokens/editing tokens etc. 78 | 79 | tokenField.tokenKeywordsList = [["France", "Germany", "Italy", "USA", "Spain", "India", "Brazil"], 80 | ["Pizza", "Pasta", "Butter Chicken", "Jamon", "Cheesecake"], 81 | ["Deer", "Dog", "Bear", "Panda", "Jaguar", "Bull"], 82 | ["Car", "Truck", "Bus", "Motorcycle", "Minivan"]] 83 | 84 | Set an array as shown above and it will show suggestions/menu based on the list above. Above list displays country names at index 0, food names at index 1, animal names at index 2 etc.. If you don't want to define token suggestions for each index separate like this, you can make use of below property to have default suggestions for all other indices except those present above. 85 | 86 | tokenField.defaultTokenKeywords = ["Red", "Blue", "Green", "White", "Purple", "Black"] 87 | 88 | This will show default suggestions and token menu for all other tokens in tokenField whose index is not specified in `tokenKeywordsList`. 89 | 90 | You can also set `leftView` property of token field to any `NSView` or subclass of `NSView`. For eg:- you can add an `NSButton` as `leftView` and have an action to set to it(See gif5). 91 | 92 | #### Setting properties 93 | 94 | //set any required properties 95 | tokenField.shouldEnableTokenMenu = true 96 | tokenField.tokenDelegate = self 97 | tokenField.leftView = lockButton 98 | 99 | tokenField.didDeleteTokenBlock = { (tokenIndex, _) in 100 | print("Token at index = ", tokenIndex, "is removed") 101 | } 102 | 103 | #### Implement delegate 104 | 105 | public func tokenField(_ tokenField: NSTokenField, completionsForSubstring substring: String, indexOfToken tokenIndex: Int, indexOfSelectedItem selectedIndex: UnsafeMutablePointer?) -> [Any]? { 106 | switch tokenIndex { 107 | case 0: 108 | return actors 109 | case 1: 110 | return actresses 111 | case 2: 112 | return movies 113 | case 3: 114 | return years 115 | default: 116 | return nil 117 | } 118 | } 119 | 120 | 121 | 122 | ## Screenshots 123 |
124 | 125 | ## License 126 | 127 | MIT License 128 | 129 | Copyright (c) 2017, Akhil C Balan(https://github.com/akhilcb) 130 | 131 | All rights reserved. 132 | 133 | 134 | -------------------------------------------------------------------------------- /Sources/ACBTokenField/Extensions/NSTokenField+ACBExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSTokenField+ACBExtension.swift 3 | // ACBTokenField 4 | // 5 | // Created by Akhil on 7/30/17. 6 | // Copyright © 2017 akhil. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | extension NSTokenField { 13 | 14 | fileprivate struct Keys { 15 | static fileprivate var tokenKeywordsList: UInt8 = 0 16 | static fileprivate var tokenFieldController: UInt8 = 0 17 | static fileprivate var tokenDelegate: UInt8 = 0 18 | static fileprivate var shouldEnableTokenMenu: UInt8 = 0 19 | static fileprivate var tokens: UInt8 = 0 20 | static fileprivate var defaultTokenKeywords: UInt8 = 0 21 | static fileprivate var leftView: UInt8 = 0 22 | static fileprivate var clearButton: UInt8 = 0 23 | static fileprivate var shouldDisplayClearButton: UInt8 = 0 24 | static fileprivate var viewPadding: UInt8 = 0 25 | static fileprivate var shouldDisplaySearchIcon: UInt8 = 0 26 | static fileprivate var clearIconName: UInt8 = 0 27 | static fileprivate var searchIconName: UInt8 = 0 28 | static fileprivate var didDeleteTokenBlock: UInt8 = 0 29 | static fileprivate var didAddTokenBlock: UInt8 = 0 30 | static fileprivate var didClearTokensBlock: UInt8 = 0 31 | } 32 | 33 | 34 | // # MARK: Public stored property 35 | 36 | public var didDeleteTokenBlock: ((NSInteger, NSTokenField) -> ())? { 37 | get { return associated(key: &Keys.didDeleteTokenBlock) ?? nil } 38 | set { 39 | associate(key: &Keys.didDeleteTokenBlock, value: newValue) 40 | self.tokenFieldController.didDeleteTokenBlock = newValue 41 | } 42 | } 43 | 44 | public var didAddTokenBlock: ((NSInteger, NSTokenField) -> ())? { 45 | get { return associated(key: &Keys.didAddTokenBlock) ?? nil } 46 | set { 47 | associate(key: &Keys.didAddTokenBlock, value: newValue) 48 | self.tokenFieldController.didAddTokenBlock = newValue 49 | } 50 | } 51 | 52 | public var didClearTokensBlock: (() -> ())? { 53 | get { return associated(key: &Keys.didClearTokensBlock) ?? nil } 54 | set { 55 | associate(key: &Keys.didClearTokensBlock, value: newValue) 56 | self.tokenFieldController.didClearTokensBlock = newValue 57 | } 58 | } 59 | 60 | public var defaultTokenKeywords: [String]? { 61 | get { return associated(key: &Keys.defaultTokenKeywords) ?? nil } 62 | set { 63 | associate(key: &Keys.defaultTokenKeywords, value: newValue) 64 | self.tokenFieldController.updateDefaultTokenMenuWithNewKeywords(newValue) 65 | } 66 | } 67 | 68 | public var tokenKeywordsList: [[String]]? { 69 | get { return associated(key: &Keys.tokenKeywordsList) ?? nil } 70 | set { 71 | associate(key: &Keys.tokenKeywordsList, value: newValue) 72 | self.tokenFieldController.updateTokenMenuWithNewKeywords(newValue) 73 | } 74 | } 75 | 76 | public var shouldEnableTokenMenu: Bool { 77 | get { return associated(key: &Keys.shouldEnableTokenMenu) { false }! } 78 | set { associate(key: &Keys.shouldEnableTokenMenu, value: newValue) 79 | if let _ = self.cell as? ACBTokenFieldCell { 80 | self.needsDisplay = true 81 | } 82 | } 83 | } 84 | 85 | public var tokenDelegate: NSTokenFieldDelegate? { 86 | get { return associated(key: &Keys.tokenDelegate) ?? nil } 87 | set { associate(key: &Keys.tokenDelegate, value: newValue) } 88 | } 89 | 90 | public var leftView: NSView? { 91 | get { return associated(key: &Keys.leftView) ?? nil } 92 | set { 93 | if let oldView: NSView = associated(key: &Keys.leftView) { 94 | oldView.removeFromSuperview() 95 | } 96 | 97 | associate(key: &Keys.leftView, value: newValue) 98 | if let cell = self.cell as? ACBTokenFieldCell { 99 | cell.shouldDisplayLeftView = (newValue != nil) 100 | if let newView = newValue { 101 | newView.frame = CGRect(x: 0, y: 0, width: 21, height: 21) 102 | self.addSubview(newView) 103 | } 104 | self.needsDisplay = true 105 | } 106 | } 107 | } 108 | 109 | public var shouldDisplayClearButton: Bool { 110 | get { return associated(key: &Keys.shouldDisplayClearButton) { true }! } 111 | set { associate(key: &Keys.shouldDisplayClearButton, value: newValue) 112 | if let cell = self.cell as? ACBTokenFieldCell { 113 | cell.shouldDisplayClearButton = newValue 114 | self.needsDisplay = true 115 | } 116 | self.handleClearButton() 117 | } 118 | } 119 | 120 | public var shouldDisplaySearchIcon: Bool { 121 | get { return associated(key: &Keys.shouldDisplaySearchIcon) { false }! } 122 | set { associate(key: &Keys.shouldDisplaySearchIcon, value: newValue) 123 | if !newValue { 124 | self.leftView = nil 125 | if let cell = self.cell as? ACBTokenFieldCell { 126 | cell.shouldDisplayLeftView = false 127 | } 128 | } else if self.leftView == nil { 129 | if let cell = self.cell as? ACBTokenFieldCell { 130 | cell.shouldDisplayLeftView = true 131 | } 132 | 133 | //if left view is present, dont change it 134 | self.setupSearchIconView() 135 | } 136 | } 137 | } 138 | 139 | public var clearIconName: String { 140 | get { return associated(key: &Keys.clearIconName) { "ClearDarkGray" }! } 141 | set { associate(key: &Keys.clearIconName, value: newValue) } 142 | } 143 | 144 | public var searchIconName: String { 145 | get { return associated(key: &Keys.searchIconName) { "Glass" }! } 146 | set { associate(key: &Keys.searchIconName, value: newValue) } 147 | } 148 | 149 | 150 | // # MARK: Public computed property 151 | 152 | public var tokenStringValue: String { 153 | var string = "" 154 | if let tokens = self.objectValue as? [ACBToken] { 155 | let stringList = tokens.map { $0.name.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) } 156 | string = stringList.joined(separator: " ") 157 | } else if let tokens = self.objectValue as? [String] { 158 | string = tokens.joined(separator: " ") 159 | } 160 | 161 | return string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 162 | } 163 | 164 | 165 | // # MARK: Private property 166 | 167 | fileprivate var tokenFieldController: ACBTokenFieldController { 168 | get { return associated(key: &Keys.tokenFieldController) { ACBTokenFieldController() }! } 169 | set { associate(key: &Keys.tokenFieldController, value: newValue) } 170 | } 171 | 172 | fileprivate var tokens: [ACBToken] { 173 | get { return associated(key: &Keys.tokens) { [] }! } 174 | set { associate(key: &Keys.tokens, value: newValue) } 175 | } 176 | 177 | fileprivate var clearButton: NSButton { 178 | get { 179 | return associated(key: &Keys.clearButton) { 180 | var button: NSButton! 181 | if #available(OSX 10.12, *) { 182 | button = NSButton(image: NSImage(named: self.clearIconName)!, 183 | target: self, 184 | action: #selector(self.clearButtonTapped(_:))) 185 | } else { 186 | // Fallback on earlier versions 187 | button = NSButton() 188 | button.image = NSImage(named: self.clearIconName)! 189 | button.target = self 190 | button.action = #selector(self.clearButtonTapped(_:)) 191 | } 192 | button.setButtonType(NSButton.ButtonType.momentaryChange) 193 | button.isBordered = false 194 | return button 195 | }! 196 | } 197 | set { associate(key: &Keys.clearButton, value: newValue) } 198 | } 199 | 200 | fileprivate var viewPadding: CGFloat { 201 | get { return associated(key: &Keys.viewPadding) { 21 }! } 202 | set { associate(key: &Keys.viewPadding, value: newValue) } 203 | } 204 | 205 | fileprivate func handleClearButton() { 206 | if shouldDisplayClearButton == false { 207 | self.clearButton.isHidden = true 208 | } else { 209 | let string = self.currentEditor()?.string ?? "" 210 | let tokens = (objectValue as? [ACBToken] ?? []) 211 | self.clearButton.isHidden = (string.count == 0) && tokens.count == 0 212 | } 213 | } 214 | 215 | 216 | // # MARK: Private method 217 | 218 | @objc private func clearButtonTapped(_ sender: Any?) { 219 | self.stringValue = "" 220 | handleClearButton() 221 | didClearTokensBlock?() 222 | } 223 | 224 | private func setupCellToField() { 225 | let isBorderedTemp = self.isBordered 226 | let backgroundColorTemp = self.backgroundColor 227 | let isBezeledTemp = self.isBezeled 228 | let bezelStyleTemp = self.bezelStyle 229 | let isEnabledTemp = self.isEnabled 230 | let isEditableTemp = self.isEditable 231 | let isSelectabletemp = self.isSelectable 232 | let placeholderAttributedStringTemp = self.placeholderAttributedString 233 | let placeholderStringTemp = self.placeholderString 234 | 235 | let cell = ACBTokenFieldCell(textCell: "") 236 | cell.padding = viewPadding 237 | self.cell = nil 238 | self.cell = cell 239 | self.isBordered = isBorderedTemp 240 | self.backgroundColor = backgroundColorTemp 241 | self.isBezeled = isBezeledTemp 242 | self.bezelStyle = bezelStyleTemp 243 | self.isEnabled = isEnabledTemp 244 | self.isEditable = isEditableTemp 245 | self.isSelectable = isSelectabletemp 246 | if placeholderAttributedStringTemp != nil { 247 | self.placeholderAttributedString = placeholderAttributedStringTemp 248 | } else if placeholderStringTemp != nil { 249 | self.placeholderString = placeholderStringTemp 250 | } else { 251 | self.placeholderString = placeholderStringTemp 252 | self.placeholderAttributedString = placeholderAttributedStringTemp 253 | } 254 | } 255 | 256 | private func setupSearchIconView() { 257 | var searchImageView: NSImageView! 258 | if #available(OSX 10.12, *) { 259 | searchImageView = NSImageView(image: NSImage(named: self.searchIconName)!) 260 | } else { 261 | // Fallback on earlier versions 262 | searchImageView = NSImageView() 263 | searchImageView.image = NSImage(named: self.searchIconName)! 264 | } 265 | self.leftView = searchImageView 266 | } 267 | 268 | private func setupConstraintOnClearButton() { 269 | let rightConstraint = NSLayoutConstraint(item: clearButton, attribute: NSLayoutConstraint.Attribute.right, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self, attribute: NSLayoutConstraint.Attribute.right, multiplier: 1, constant: 0) 270 | let verticalConstraint = NSLayoutConstraint(item: clearButton, attribute: NSLayoutConstraint.Attribute.centerY, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self, attribute: NSLayoutConstraint.Attribute.centerY, multiplier: 1, constant: 0) 271 | let widthConstraint = NSLayoutConstraint(item: clearButton, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: viewPadding) 272 | let heightConstraint = NSLayoutConstraint(item: clearButton, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: viewPadding) 273 | self.addConstraints([rightConstraint, verticalConstraint, widthConstraint, heightConstraint]) 274 | } 275 | 276 | 277 | // # MARK: Public method 278 | 279 | public func convertToACBTokenField() { 280 | tokenFieldController.tokenField = self 281 | setupCellToField() 282 | 283 | shouldDisplayClearButton = true 284 | let x = self.frame.size.width - viewPadding - 3 285 | let y = (self.frame.size.height - viewPadding) / 2 286 | clearButton.frame = CGRect(x: x, 287 | y: y, 288 | width: viewPadding, 289 | height: viewPadding) 290 | clearButton.translatesAutoresizingMaskIntoConstraints = false 291 | self.addSubview(clearButton) 292 | setupConstraintOnClearButton() 293 | 294 | handleClearButton() 295 | self.delegate = tokenFieldController 296 | } 297 | 298 | public func addToken(name: String) { 299 | var currentTokens: [ACBToken] 300 | if let array = self.objectValue as? [ACBToken] { 301 | currentTokens = array 302 | } else { 303 | currentTokens = [] 304 | } 305 | 306 | let token = ACBToken(name: name) 307 | currentTokens.append(token) 308 | 309 | self.objectValue = currentTokens 310 | 311 | if let fieldEditor = currentEditor() { 312 | fieldEditor.selectedRange = NSMakeRange(fieldEditor.string.count, 0) 313 | } 314 | 315 | handleClearButton() 316 | } 317 | 318 | //calculate token index based on currentEditor's selectedRange and string 319 | public func selectedTokenIndex() -> Int? { 320 | var tokenIndex = 0 321 | let selectedRange = self.currentEditor()?.selectedRange 322 | let rangeLocation = selectedRange?.location ?? 0 323 | 324 | let string = self.currentEditor()?.string as NSString? 325 | if let subString = string?.substring(to: rangeLocation) as NSString? { 326 | let maxIndex = subString.length 327 | 328 | for i in 0.. Int? { 341 | guard let token = representedObject as? ACBToken else { 342 | 343 | return nil 344 | } 345 | 346 | let tokens = self.tokens 347 | let tokenIndex = tokens.firstIndex(of: token) 348 | 349 | return tokenIndex 350 | } 351 | 352 | public func resetTokens() { 353 | self.objectValue = nil 354 | self.tokens = [] 355 | } 356 | 357 | public var tokenCount: Int { 358 | return self.tokenFieldController.tokenCount() 359 | } 360 | } 361 | 362 | 363 | fileprivate class ACBTokenFieldController: NSObject, NSTokenFieldDelegate { 364 | 365 | fileprivate weak var tokenField: NSTokenField? 366 | fileprivate var tokenMenuList: [NSMenu]? 367 | fileprivate var defaultTokenMenu: NSMenu? 368 | fileprivate var didDeleteTokenBlock: ((NSInteger, NSTokenField) -> ())? 369 | fileprivate var didAddTokenBlock: ((NSInteger, NSTokenField) -> ())? 370 | fileprivate var didClearTokensBlock: (() -> ())? 371 | 372 | private var prevTokenCount: Int = 0 373 | 374 | //TODO: This doesn't work due to a swift compiler issue since tokenField is weak 375 | // private static var tokenContext = 0 376 | 377 | // @objc var tokenField: NSTokenField? { 378 | // didSet { 379 | // setupObservers() 380 | // } 381 | // } 382 | // 383 | // deinit { 384 | // removeObserver(self, forKeyPath: #keyPath(tokenField.delegate), context: &ACBTokenFieldController.tokenContext) 385 | // } 386 | // 387 | // func setupObservers() { 388 | // self.addObserver(self, forKeyPath: #keyPath(tokenField.delegate), options: [.old, .new], context: &ACBTokenFieldController.tokenContext) 389 | // } 390 | // 391 | // override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 392 | // if context == &ACBTokenFieldController.tokenContext { 393 | // if keyPath == #keyPath(tokenField.delegate) { 394 | // if (tokenField!.delegate is ACBTokenFieldController) == false { 395 | // tokenField!.tokenDelegate = tokenField!.delegate 396 | // tokenField!.delegate = self 397 | // } 398 | // } 399 | // } 400 | // } 401 | 402 | // # MARK: Forward Invocation methods 403 | 404 | override func forwardingTarget(for aSelector: Selector!) -> Any? { 405 | if let tokenDelegate = tokenField?.tokenDelegate, 406 | tokenDelegate.responds(to: aSelector) { 407 | 408 | return tokenDelegate 409 | } 410 | 411 | return super.forwardingTarget(for: aSelector) 412 | } 413 | 414 | 415 | override func responds(to aSelector: Selector!) -> Bool { 416 | 417 | if let tokenDelegate = tokenField?.tokenDelegate { 418 | 419 | return tokenDelegate.responds(to: aSelector) || super.responds(to: aSelector) 420 | } 421 | 422 | return super.responds(to: aSelector) 423 | } 424 | 425 | func controlTextDidBeginEditing(_ obj: Notification) { 426 | prevTokenCount = tokenCount() 427 | } 428 | 429 | func controlTextDidChange(_ obj: Notification) { 430 | tokenField?.handleClearButton() 431 | 432 | DispatchQueue.main.async { [weak self] in 433 | if let tokenField = self?.tokenField, 434 | let prevCount = self?.prevTokenCount, 435 | let newCount = self?.tokenCount() { 436 | if prevCount > newCount { 437 | let index = tokenField.selectedTokenIndex() ?? newCount 438 | self?.didDeleteTokenBlock?(index, tokenField) 439 | } else if prevCount < newCount { 440 | let index = max(0, (tokenField.selectedTokenIndex() ?? newCount) - 1) 441 | self?.didAddTokenBlock?(index, tokenField) 442 | } 443 | self?.prevTokenCount = newCount 444 | } 445 | } 446 | } 447 | 448 | 449 | // # MARK: NSTokenFieldDelegate methods 450 | 451 | func tokenField(_ tokenField: NSTokenField, 452 | completionsForSubstring substring: String, 453 | indexOfToken tokenIndex: Int, 454 | indexOfSelectedItem selectedIndex: UnsafeMutablePointer?) -> [Any]? { 455 | //tokenIndex is always returning 0 due to an appkit bug 456 | let newTokenIndex = tokenField.selectedTokenIndex() ?? 0 457 | 458 | if let tokenKeywordsList = tokenField.tokenKeywordsList, 459 | tokenKeywordsList.count > newTokenIndex { 460 | let tokenKeywords = tokenKeywordsList[newTokenIndex] 461 | let filtered = tokenKeywords.filter { $0.lowercased().hasPrefix(substring.lowercased()) } 462 | 463 | return filtered 464 | } else if let tokenKeywords = tokenField.defaultTokenKeywords { 465 | let filtered = tokenKeywords.filter { $0.lowercased().hasPrefix(substring.lowercased()) } 466 | 467 | return filtered 468 | } else if let tokenDelegate = tokenField.tokenDelegate, 469 | tokenDelegate.responds(to: #selector(tokenField(_:completionsForSubstring:indexOfToken:indexOfSelectedItem:))) { 470 | if let tokenKeywords = tokenDelegate.tokenField!(tokenField, 471 | completionsForSubstring: substring, 472 | indexOfToken: newTokenIndex, 473 | indexOfSelectedItem: selectedIndex) as? [String] { 474 | 475 | let filtered = tokenKeywords.filter { $0.lowercased().hasPrefix(substring.lowercased()) } 476 | return filtered 477 | } 478 | } 479 | 480 | return nil 481 | } 482 | 483 | 484 | func tokenField(_ tokenField: NSTokenField, 485 | displayStringForRepresentedObject representedObject: Any) -> String? { 486 | if let token = representedObject as? ACBToken { 487 | return token.name 488 | } 489 | 490 | if let string = representedObject as? String { 491 | return string 492 | } else if let tokenDelegate = tokenField.tokenDelegate, 493 | tokenDelegate.responds(to: #selector(tokenField(_:displayStringForRepresentedObject:))) { 494 | if let string = tokenDelegate.tokenField!(tokenField, 495 | displayStringForRepresentedObject:representedObject) { 496 | 497 | return string 498 | } 499 | } 500 | 501 | return nil 502 | } 503 | 504 | 505 | func tokenField(_ tokenField: NSTokenField, 506 | hasMenuForRepresentedObject representedObject: Any) -> Bool { 507 | guard let tokenField = self.tokenField, 508 | (tokenField.shouldEnableTokenMenu == true) else { 509 | 510 | return false 511 | } 512 | 513 | let tokenMenu = tokenMenuFor(representedObject: representedObject) 514 | if (tokenMenu != nil) { 515 | 516 | return true 517 | } else if let tokenDelegate = tokenField.tokenDelegate, 518 | tokenDelegate.responds(to: #selector(tokenField(_:hasMenuForRepresentedObject:))) { 519 | 520 | let hasMenu = tokenDelegate.tokenField!(tokenField, 521 | hasMenuForRepresentedObject:representedObject) 522 | return hasMenu 523 | } 524 | 525 | return false 526 | } 527 | 528 | 529 | func tokenField(_ tokenField: NSTokenField, 530 | menuForRepresentedObject representedObject: Any) -> NSMenu? { 531 | let tokenMenu = tokenMenuFor(representedObject: representedObject) 532 | updateTokenMenuState(tokenMenu: tokenMenu, representedObject: representedObject) 533 | 534 | if (tokenMenu != nil) { 535 | return tokenMenu 536 | } else if let tokenDelegate = tokenField.tokenDelegate, 537 | tokenDelegate.responds(to: #selector(tokenField(_:menuForRepresentedObject:))) { 538 | 539 | let menu = tokenDelegate.tokenField!(tokenField, 540 | menuForRepresentedObject:representedObject) 541 | return menu 542 | } 543 | 544 | return nil 545 | } 546 | 547 | 548 | func tokenField(_ tokenField: NSTokenField, 549 | representedObjectForEditing editingString: String) -> (Any)? { 550 | return ACBToken(name: editingString) 551 | } 552 | 553 | 554 | func tokenField(_ tokenField: NSTokenField, shouldAdd tokens: [Any], at index: Int) -> [Any] { 555 | DispatchQueue.main.async { 556 | if let objects = tokenField.objectValue as? [ACBToken] { 557 | tokenField.tokens = objects 558 | } 559 | } 560 | 561 | if let tokenDelegate = tokenField.tokenDelegate, 562 | tokenDelegate.responds(to: #selector(tokenField(_:shouldAdd:at:))) { 563 | 564 | let newTokens = tokenDelegate.tokenField!(tokenField, 565 | shouldAdd:tokens,at:index) 566 | return newTokens 567 | } 568 | 569 | return tokens 570 | } 571 | 572 | 573 | // # MARK: Private methods 574 | 575 | //return token menu associated with a clicked token 576 | private func tokenMenuFor(representedObject: Any) -> NSMenu? { 577 | let tokenIndex = tokenField!.tokenIndex(forRepresentedObject: representedObject) ?? 0 578 | 579 | if let tokenMenuList = self.tokenMenuList, 580 | tokenMenuList.count > tokenIndex { 581 | let tokenMenu = tokenMenuList[tokenIndex] 582 | 583 | return tokenMenu 584 | } else { 585 | 586 | return self.defaultTokenMenu 587 | } 588 | } 589 | 590 | //update token menu state to on if the menu item name matches clicked token 591 | private func updateTokenMenuState(tokenMenu: NSMenu?, representedObject: Any) { 592 | guard let tokenMenu = tokenMenu else { 593 | return 594 | } 595 | 596 | if let token = representedObject as? ACBToken { 597 | tokenMenu.items.forEach { menuItem in 598 | menuItem.state = (menuItem.title == token.name) ? .on : .off 599 | } 600 | } else if let token = representedObject as? String { 601 | tokenMenu.items.forEach { menuItem in 602 | menuItem.state = (menuItem.title == token) ? .on : .off 603 | } 604 | } 605 | } 606 | 607 | @objc private func menuItemTapped(_ menuItem: NSMenuItem) { 608 | if let fieldEditor = tokenField?.currentEditor() { 609 | let textRange = fieldEditor.selectedRange 610 | let replaceString = menuItem.title 611 | fieldEditor.replaceCharacters(in: textRange, with: replaceString) 612 | fieldEditor.selectedRange = NSMakeRange(textRange.location, replaceString.count) 613 | } 614 | } 615 | 616 | fileprivate func tokenCount() -> Int { 617 | var newCount: Int = 0 618 | 619 | if let string = tokenField?.currentEditor()?.string as NSString? { 620 | let maxIndex = string.length 621 | for i in 0.. NSRect { 689 | var titleFrame = rect 690 | let newPadding = padding - 3 691 | 692 | if shouldDisplayLeftView { 693 | var origin = titleFrame.origin 694 | origin.x += newPadding 695 | titleFrame.origin = origin 696 | } 697 | 698 | var size = titleFrame.size 699 | 700 | if shouldDisplayClearButton { 701 | size.width -= newPadding 702 | } 703 | if shouldDisplayLeftView { 704 | size.width -= newPadding 705 | } 706 | 707 | titleFrame.size = size 708 | 709 | return titleFrame 710 | } 711 | 712 | override func drawingRect(forBounds rect: NSRect) -> NSRect { 713 | let titleRect = tokenFieldRect(forBounds: rect) 714 | let newRect = super.drawingRect(forBounds: titleRect) 715 | return newRect 716 | } 717 | 718 | } 719 | -------------------------------------------------------------------------------- /Sources/ACBTokenField/Helpers/ACBAssociation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACBAssociation.swift 3 | // ACBTokenField 4 | // 5 | // Created by Akhil on 7/30/17. 6 | // Copyright © 2017 akhil. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ObjectiveC 11 | 12 | extension NSObject { 13 | 14 | func associate(key: UnsafeRawPointer, 15 | value: T, 16 | policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC) { 17 | objc_setAssociatedObject(self, key, value, policy) 18 | } 19 | 20 | func associated(key: UnsafeRawPointer, 21 | initializer: (() -> T)? = nil) -> T? { 22 | if let value = objc_getAssociatedObject(self, key) as? T { 23 | 24 | return value 25 | } 26 | 27 | if let initializer = initializer { 28 | let newValue = initializer() 29 | associate(key: key, value: newValue) 30 | 31 | return newValue 32 | } 33 | 34 | return nil 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ACBTokenField/Models/ACBToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACBToken.swift 3 | // ACBTokenField 4 | // 5 | // Created by Akhil on 7/30/17. 6 | // Copyright © 2017 akhil. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | public class ACBToken: NSObject, NSCoding { 12 | 13 | // # MARK: Public 14 | 15 | public var name: String 16 | 17 | // # MARK: Public Method 18 | 19 | init(name: String) { 20 | self.name = name 21 | } 22 | 23 | // # MARK: NSCoding 24 | 25 | public func encode(with aCoder: NSCoder) { 26 | aCoder.encode(self.name) 27 | } 28 | 29 | public required init?(coder aDecoder: NSCoder) { 30 | if let name = aDecoder.decodeObject() as? String { 31 | self.name = name 32 | } else { 33 | self.name = "" 34 | } 35 | } 36 | 37 | } 38 | --------------------------------------------------------------------------------