├── .gitignore ├── CLTokenInputView.podspec ├── CLTokenInputView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── rizwan.xcuserdatad │ └── xcschemes │ ├── CLTokenInputView.xcscheme │ └── xcschememanagement.plist ├── CLTokenInputView ├── CLAppDelegate.h ├── CLAppDelegate.m ├── CLTokenInputView-Info.plist ├── CLTokenInputView-Prefix.pch ├── CLTokenInputView │ ├── CLBackspaceDetectingTextField.h │ ├── CLBackspaceDetectingTextField.m │ ├── CLToken.h │ ├── CLToken.m │ ├── CLTokenInputView.h │ ├── CLTokenInputView.m │ ├── CLTokenView.h │ └── CLTokenView.m ├── CLTokenInputViewController.h ├── CLTokenInputViewController.m ├── CLTokenInputViewController.xib ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── LaunchScreen.storyboard ├── en.lproj │ └── InfoPlist.strings └── main.m ├── CLTokenInputViewTests ├── CLTokenInputViewTests-Info.plist ├── CLTokenInputViewTests.m └── en.lproj │ └── InfoPlist.strings ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xccheckout 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | DerivedData 17 | .idea/ 18 | -------------------------------------------------------------------------------- /CLTokenInputView.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint CLTokenInputView.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | 11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 12 | # 13 | # These will help people to find your library, and whilst it 14 | # can feel like a chore to fill in it's definitely to your advantage. The 15 | # summary should be tweet-length, and the description more in depth. 16 | # 17 | 18 | s.name = "CLTokenInputView" 19 | s.version = "2.4.0" 20 | s.summary = "A replica of iOS's native contact bubbles UI." 21 | 22 | s.description = <<-DESC 23 | CLTokenInputView is an almost pixel-perfect replica of iOS's contact bubbles 24 | input UI (seen in Mail.app and Messages.app). It *does not* implement any 25 | autocomplete UI, just the UI where you can enter text into a text field and 26 | bubbles which are deletable using the backspace key. 27 | 28 | Check out the sample view controller which uses CLTokenInputView to see how to 29 | incorporate it into your UI. We use this in our apps at [Cluster Labs, Inc.](https://cluster.co). 30 | 31 | Things I'd like to maybe add in the future (or you can help contribute): 32 | * Build the "collapsed" mode like in Mail.app which replaces the token UI with 33 | "[first-item] and N more" 34 | * Call search about 150ms after pausing typing 35 | * Scroll text field into position after typing 36 | * (Maybe?) Look into adding a very generic, flexible autocomplete UI? 37 | DESC 38 | 39 | s.homepage = "https://github.com/ClusterInc/CLTokenInputView" 40 | # s.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif" 41 | 42 | 43 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 44 | # 45 | # Licensing your code is important. See http://choosealicense.com for more info. 46 | # CocoaPods will detect a license file if there is a named LICENSE* 47 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'. 48 | # 49 | 50 | s.license = { :type => "MIT", :file => "LICENSE" } 51 | 52 | 53 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 54 | # 55 | # Specify the authors of the library, with email addresses. Email addresses 56 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also 57 | # accepts just a name if you'd rather not provide an email address. 58 | # 59 | # Specify a social_media_url where others can refer to, for example a twitter 60 | # profile URL. 61 | # 62 | 63 | s.author = { "Rizwan Sattar" => "rsattar@gmail.com" } 64 | # Or just: s.author = "Rizwan Sattar" 65 | # s.authors = { "Rizwan Sattar" => "rsattar@gmail.com" } 66 | s.social_media_url = "http://twitter.com/rizzledizzle" 67 | 68 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 69 | # 70 | # If this Pod runs only on iOS or OS X, then specify the platform and 71 | # the deployment target. You can optionally include the target after the platform. 72 | # 73 | 74 | s.platform = :ios, "7.0" 75 | 76 | # When using multiple platforms 77 | # s.ios.deployment_target = "5.0" 78 | # s.osx.deployment_target = "10.7" 79 | 80 | 81 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 82 | # 83 | # Specify the location from where the source should be retrieved. 84 | # Supports git, hg, bzr, svn and HTTP. 85 | # 86 | 87 | s.source = { :git => "https://github.com/ClusterInc/CLTokenInputView.git", :tag => s.version.to_s } 88 | 89 | 90 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 91 | # 92 | # CocoaPods is smart about how it includes source code. For source files 93 | # giving a folder will include any h, m, mm, c & cpp files. For header 94 | # files it will include any header in the folder. 95 | # Not including the public_header_files will make all headers public. 96 | # 97 | 98 | s.source_files = "CLTokenInputView/CLTokenInputView", "CLTokenInputView/CLTokenInputView/**/*.{h,m}" 99 | s.exclude_files = "CLTokenInputView/CLTokenInputView/Exclude" 100 | 101 | # s.public_header_files = "Classes/**/*.h" 102 | 103 | 104 | # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 105 | # 106 | # A list of resources included with the Pod. These are copied into the 107 | # target bundle with a build phase script. Anything else will be cleaned. 108 | # You can preserve files from being cleaned, please don't preserve 109 | # non-essential files like tests, examples and documentation. 110 | # 111 | 112 | # s.resource = "icon.png" 113 | # s.resources = "Resources/*.png" 114 | 115 | # s.preserve_paths = "FilesToSave", "MoreFilesToSave" 116 | 117 | 118 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 119 | # 120 | # Link your library with frameworks, or libraries. Libraries do not include 121 | # the lib prefix of their name. 122 | # 123 | 124 | # s.framework = "SomeFramework" 125 | # s.frameworks = "SomeFramework", "AnotherFramework" 126 | 127 | # s.library = "iconv" 128 | # s.libraries = "iconv", "xml2" 129 | 130 | 131 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 132 | # 133 | # If your library depends on compiler flags you can set them in the xcconfig hash 134 | # where they will only apply to your library. If you depend on other Podspecs 135 | # you can include multiple dependencies to ensure it works. 136 | 137 | s.requires_arc = true 138 | 139 | # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } 140 | # s.dependency "JSONKit", "~> 1.4" 141 | 142 | end 143 | -------------------------------------------------------------------------------- /CLTokenInputView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 65B16B7E18BC16F6003AA819 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65B16B7D18BC16F6003AA819 /* Foundation.framework */; }; 11 | 65B16B8018BC16F6003AA819 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65B16B7F18BC16F6003AA819 /* CoreGraphics.framework */; }; 12 | 65B16B8218BC16F6003AA819 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65B16B8118BC16F6003AA819 /* UIKit.framework */; }; 13 | 65B16B8818BC16F6003AA819 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 65B16B8618BC16F6003AA819 /* InfoPlist.strings */; }; 14 | 65B16B8A18BC16F6003AA819 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16B8918BC16F6003AA819 /* main.m */; }; 15 | 65B16B8E18BC16F6003AA819 /* CLAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16B8D18BC16F6003AA819 /* CLAppDelegate.m */; }; 16 | 65B16B9018BC16F6003AA819 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 65B16B8F18BC16F6003AA819 /* Images.xcassets */; }; 17 | 65B16B9718BC16F7003AA819 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65B16B9618BC16F7003AA819 /* XCTest.framework */; }; 18 | 65B16B9818BC16F7003AA819 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65B16B7D18BC16F6003AA819 /* Foundation.framework */; }; 19 | 65B16B9918BC16F7003AA819 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65B16B8118BC16F6003AA819 /* UIKit.framework */; }; 20 | 65B16BA118BC16F7003AA819 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 65B16B9F18BC16F7003AA819 /* InfoPlist.strings */; }; 21 | 65B16BA318BC16F7003AA819 /* CLTokenInputViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16BA218BC16F7003AA819 /* CLTokenInputViewTests.m */; }; 22 | 65B16BAF18BC17D2003AA819 /* CLTokenInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16BAE18BC17D2003AA819 /* CLTokenInputView.m */; }; 23 | 65B16BB018BC17D2003AA819 /* CLTokenInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16BAE18BC17D2003AA819 /* CLTokenInputView.m */; }; 24 | 65B16BB318BC17EC003AA819 /* CLTokenView.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16BB218BC17EC003AA819 /* CLTokenView.m */; }; 25 | 65B16BB418BC17EC003AA819 /* CLTokenView.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16BB218BC17EC003AA819 /* CLTokenView.m */; }; 26 | 65B16BB818BC1826003AA819 /* CLTokenInputViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16BB618BC1826003AA819 /* CLTokenInputViewController.m */; }; 27 | 65B16BB918BC1826003AA819 /* CLTokenInputViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16BB618BC1826003AA819 /* CLTokenInputViewController.m */; }; 28 | 65B16BBA18BC1826003AA819 /* CLTokenInputViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 65B16BB718BC1826003AA819 /* CLTokenInputViewController.xib */; }; 29 | 65B16BBB18BC1826003AA819 /* CLTokenInputViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 65B16BB718BC1826003AA819 /* CLTokenInputViewController.xib */; }; 30 | 65B16BBE18BC1D9E003AA819 /* CLBackspaceDetectingTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16BBD18BC1D9E003AA819 /* CLBackspaceDetectingTextField.m */; }; 31 | 65B16BBF18BC1D9E003AA819 /* CLBackspaceDetectingTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16BBD18BC1D9E003AA819 /* CLBackspaceDetectingTextField.m */; }; 32 | 65B16BC218BC26C9003AA819 /* CLToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16BC118BC26C9003AA819 /* CLToken.m */; }; 33 | 65B16BC318BC26C9003AA819 /* CLToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B16BC118BC26C9003AA819 /* CLToken.m */; }; 34 | 65E86F821C407E3A0029E724 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 65E86F811C407E3A0029E724 /* LaunchScreen.storyboard */; }; 35 | /* End PBXBuildFile section */ 36 | 37 | /* Begin PBXContainerItemProxy section */ 38 | 65B16B9A18BC16F7003AA819 /* PBXContainerItemProxy */ = { 39 | isa = PBXContainerItemProxy; 40 | containerPortal = 65B16B7218BC16F6003AA819 /* Project object */; 41 | proxyType = 1; 42 | remoteGlobalIDString = 65B16B7918BC16F6003AA819; 43 | remoteInfo = CLTokenInputView; 44 | }; 45 | /* End PBXContainerItemProxy section */ 46 | 47 | /* Begin PBXFileReference section */ 48 | 65B16B7A18BC16F6003AA819 /* CLTokenInputView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CLTokenInputView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 65B16B7D18BC16F6003AA819 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 50 | 65B16B7F18BC16F6003AA819 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 51 | 65B16B8118BC16F6003AA819 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 52 | 65B16B8518BC16F6003AA819 /* CLTokenInputView-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "CLTokenInputView-Info.plist"; sourceTree = ""; }; 53 | 65B16B8718BC16F6003AA819 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 54 | 65B16B8918BC16F6003AA819 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 55 | 65B16B8B18BC16F6003AA819 /* CLTokenInputView-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CLTokenInputView-Prefix.pch"; sourceTree = ""; }; 56 | 65B16B8C18BC16F6003AA819 /* CLAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CLAppDelegate.h; sourceTree = ""; }; 57 | 65B16B8D18BC16F6003AA819 /* CLAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CLAppDelegate.m; sourceTree = ""; }; 58 | 65B16B8F18BC16F6003AA819 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 59 | 65B16B9518BC16F7003AA819 /* CLTokenInputViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CLTokenInputViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 60 | 65B16B9618BC16F7003AA819 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 61 | 65B16B9E18BC16F7003AA819 /* CLTokenInputViewTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "CLTokenInputViewTests-Info.plist"; sourceTree = ""; }; 62 | 65B16BA018BC16F7003AA819 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 63 | 65B16BA218BC16F7003AA819 /* CLTokenInputViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CLTokenInputViewTests.m; sourceTree = ""; }; 64 | 65B16BAD18BC17D2003AA819 /* CLTokenInputView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CLTokenInputView.h; path = CLTokenInputView/CLTokenInputView.h; sourceTree = ""; }; 65 | 65B16BAE18BC17D2003AA819 /* CLTokenInputView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CLTokenInputView.m; path = CLTokenInputView/CLTokenInputView.m; sourceTree = ""; }; 66 | 65B16BB118BC17EC003AA819 /* CLTokenView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CLTokenView.h; path = CLTokenInputView/CLTokenView.h; sourceTree = ""; }; 67 | 65B16BB218BC17EC003AA819 /* CLTokenView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CLTokenView.m; path = CLTokenInputView/CLTokenView.m; sourceTree = ""; }; 68 | 65B16BB518BC1826003AA819 /* CLTokenInputViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CLTokenInputViewController.h; sourceTree = ""; }; 69 | 65B16BB618BC1826003AA819 /* CLTokenInputViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CLTokenInputViewController.m; sourceTree = ""; }; 70 | 65B16BB718BC1826003AA819 /* CLTokenInputViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CLTokenInputViewController.xib; sourceTree = ""; }; 71 | 65B16BBC18BC1D9E003AA819 /* CLBackspaceDetectingTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CLBackspaceDetectingTextField.h; path = CLTokenInputView/CLBackspaceDetectingTextField.h; sourceTree = ""; }; 72 | 65B16BBD18BC1D9E003AA819 /* CLBackspaceDetectingTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CLBackspaceDetectingTextField.m; path = CLTokenInputView/CLBackspaceDetectingTextField.m; sourceTree = ""; }; 73 | 65B16BC018BC26C9003AA819 /* CLToken.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CLToken.h; path = CLTokenInputView/CLToken.h; sourceTree = ""; }; 74 | 65B16BC118BC26C9003AA819 /* CLToken.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CLToken.m; path = CLTokenInputView/CLToken.m; sourceTree = ""; }; 75 | 65E86F811C407E3A0029E724 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 76 | /* End PBXFileReference section */ 77 | 78 | /* Begin PBXFrameworksBuildPhase section */ 79 | 65B16B7718BC16F6003AA819 /* Frameworks */ = { 80 | isa = PBXFrameworksBuildPhase; 81 | buildActionMask = 2147483647; 82 | files = ( 83 | 65B16B8018BC16F6003AA819 /* CoreGraphics.framework in Frameworks */, 84 | 65B16B8218BC16F6003AA819 /* UIKit.framework in Frameworks */, 85 | 65B16B7E18BC16F6003AA819 /* Foundation.framework in Frameworks */, 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | 65B16B9218BC16F7003AA819 /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | 65B16B9718BC16F7003AA819 /* XCTest.framework in Frameworks */, 94 | 65B16B9918BC16F7003AA819 /* UIKit.framework in Frameworks */, 95 | 65B16B9818BC16F7003AA819 /* Foundation.framework in Frameworks */, 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | /* End PBXFrameworksBuildPhase section */ 100 | 101 | /* Begin PBXGroup section */ 102 | 65B16B7118BC16F6003AA819 = { 103 | isa = PBXGroup; 104 | children = ( 105 | 65B16B8318BC16F6003AA819 /* CLTokenInputView */, 106 | 65B16B9C18BC16F7003AA819 /* CLTokenInputViewTests */, 107 | 65B16B7C18BC16F6003AA819 /* Frameworks */, 108 | 65B16B7B18BC16F6003AA819 /* Products */, 109 | ); 110 | sourceTree = ""; 111 | }; 112 | 65B16B7B18BC16F6003AA819 /* Products */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 65B16B7A18BC16F6003AA819 /* CLTokenInputView.app */, 116 | 65B16B9518BC16F7003AA819 /* CLTokenInputViewTests.xctest */, 117 | ); 118 | name = Products; 119 | sourceTree = ""; 120 | }; 121 | 65B16B7C18BC16F6003AA819 /* Frameworks */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 65B16B7D18BC16F6003AA819 /* Foundation.framework */, 125 | 65B16B7F18BC16F6003AA819 /* CoreGraphics.framework */, 126 | 65B16B8118BC16F6003AA819 /* UIKit.framework */, 127 | 65B16B9618BC16F7003AA819 /* XCTest.framework */, 128 | ); 129 | name = Frameworks; 130 | sourceTree = ""; 131 | }; 132 | 65B16B8318BC16F6003AA819 /* CLTokenInputView */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 65B16BAC18BC17A5003AA819 /* CLTokenInputView */, 136 | 65B16B8C18BC16F6003AA819 /* CLAppDelegate.h */, 137 | 65B16B8D18BC16F6003AA819 /* CLAppDelegate.m */, 138 | 65B16BB518BC1826003AA819 /* CLTokenInputViewController.h */, 139 | 65B16BB618BC1826003AA819 /* CLTokenInputViewController.m */, 140 | 65B16BB718BC1826003AA819 /* CLTokenInputViewController.xib */, 141 | 65E86F811C407E3A0029E724 /* LaunchScreen.storyboard */, 142 | 65B16B8F18BC16F6003AA819 /* Images.xcassets */, 143 | 65B16B8418BC16F6003AA819 /* Supporting Files */, 144 | ); 145 | path = CLTokenInputView; 146 | sourceTree = ""; 147 | }; 148 | 65B16B8418BC16F6003AA819 /* Supporting Files */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 65B16B8518BC16F6003AA819 /* CLTokenInputView-Info.plist */, 152 | 65B16B8618BC16F6003AA819 /* InfoPlist.strings */, 153 | 65B16B8918BC16F6003AA819 /* main.m */, 154 | 65B16B8B18BC16F6003AA819 /* CLTokenInputView-Prefix.pch */, 155 | ); 156 | name = "Supporting Files"; 157 | sourceTree = ""; 158 | }; 159 | 65B16B9C18BC16F7003AA819 /* CLTokenInputViewTests */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 65B16BA218BC16F7003AA819 /* CLTokenInputViewTests.m */, 163 | 65B16B9D18BC16F7003AA819 /* Supporting Files */, 164 | ); 165 | path = CLTokenInputViewTests; 166 | sourceTree = ""; 167 | }; 168 | 65B16B9D18BC16F7003AA819 /* Supporting Files */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | 65B16B9E18BC16F7003AA819 /* CLTokenInputViewTests-Info.plist */, 172 | 65B16B9F18BC16F7003AA819 /* InfoPlist.strings */, 173 | ); 174 | name = "Supporting Files"; 175 | sourceTree = ""; 176 | }; 177 | 65B16BAC18BC17A5003AA819 /* CLTokenInputView */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 65B16BAD18BC17D2003AA819 /* CLTokenInputView.h */, 181 | 65B16BAE18BC17D2003AA819 /* CLTokenInputView.m */, 182 | 65B16BB118BC17EC003AA819 /* CLTokenView.h */, 183 | 65B16BB218BC17EC003AA819 /* CLTokenView.m */, 184 | 65B16BBC18BC1D9E003AA819 /* CLBackspaceDetectingTextField.h */, 185 | 65B16BBD18BC1D9E003AA819 /* CLBackspaceDetectingTextField.m */, 186 | 65B16BC018BC26C9003AA819 /* CLToken.h */, 187 | 65B16BC118BC26C9003AA819 /* CLToken.m */, 188 | ); 189 | name = CLTokenInputView; 190 | sourceTree = ""; 191 | }; 192 | /* End PBXGroup section */ 193 | 194 | /* Begin PBXNativeTarget section */ 195 | 65B16B7918BC16F6003AA819 /* CLTokenInputView */ = { 196 | isa = PBXNativeTarget; 197 | buildConfigurationList = 65B16BA618BC16F7003AA819 /* Build configuration list for PBXNativeTarget "CLTokenInputView" */; 198 | buildPhases = ( 199 | 65B16B7618BC16F6003AA819 /* Sources */, 200 | 65B16B7718BC16F6003AA819 /* Frameworks */, 201 | 65B16B7818BC16F6003AA819 /* Resources */, 202 | ); 203 | buildRules = ( 204 | ); 205 | dependencies = ( 206 | ); 207 | name = CLTokenInputView; 208 | productName = CLTokenInputView; 209 | productReference = 65B16B7A18BC16F6003AA819 /* CLTokenInputView.app */; 210 | productType = "com.apple.product-type.application"; 211 | }; 212 | 65B16B9418BC16F7003AA819 /* CLTokenInputViewTests */ = { 213 | isa = PBXNativeTarget; 214 | buildConfigurationList = 65B16BA918BC16F7003AA819 /* Build configuration list for PBXNativeTarget "CLTokenInputViewTests" */; 215 | buildPhases = ( 216 | 65B16B9118BC16F7003AA819 /* Sources */, 217 | 65B16B9218BC16F7003AA819 /* Frameworks */, 218 | 65B16B9318BC16F7003AA819 /* Resources */, 219 | ); 220 | buildRules = ( 221 | ); 222 | dependencies = ( 223 | 65B16B9B18BC16F7003AA819 /* PBXTargetDependency */, 224 | ); 225 | name = CLTokenInputViewTests; 226 | productName = CLTokenInputViewTests; 227 | productReference = 65B16B9518BC16F7003AA819 /* CLTokenInputViewTests.xctest */; 228 | productType = "com.apple.product-type.bundle.unit-test"; 229 | }; 230 | /* End PBXNativeTarget section */ 231 | 232 | /* Begin PBXProject section */ 233 | 65B16B7218BC16F6003AA819 /* Project object */ = { 234 | isa = PBXProject; 235 | attributes = { 236 | CLASSPREFIX = CL; 237 | LastUpgradeCheck = 0700; 238 | ORGANIZATIONNAME = "Cluster Labs, Inc."; 239 | TargetAttributes = { 240 | 65B16B9418BC16F7003AA819 = { 241 | TestTargetID = 65B16B7918BC16F6003AA819; 242 | }; 243 | }; 244 | }; 245 | buildConfigurationList = 65B16B7518BC16F6003AA819 /* Build configuration list for PBXProject "CLTokenInputView" */; 246 | compatibilityVersion = "Xcode 3.2"; 247 | developmentRegion = English; 248 | hasScannedForEncodings = 0; 249 | knownRegions = ( 250 | en, 251 | ); 252 | mainGroup = 65B16B7118BC16F6003AA819; 253 | productRefGroup = 65B16B7B18BC16F6003AA819 /* Products */; 254 | projectDirPath = ""; 255 | projectRoot = ""; 256 | targets = ( 257 | 65B16B7918BC16F6003AA819 /* CLTokenInputView */, 258 | 65B16B9418BC16F7003AA819 /* CLTokenInputViewTests */, 259 | ); 260 | }; 261 | /* End PBXProject section */ 262 | 263 | /* Begin PBXResourcesBuildPhase section */ 264 | 65B16B7818BC16F6003AA819 /* Resources */ = { 265 | isa = PBXResourcesBuildPhase; 266 | buildActionMask = 2147483647; 267 | files = ( 268 | 65B16B8818BC16F6003AA819 /* InfoPlist.strings in Resources */, 269 | 65E86F821C407E3A0029E724 /* LaunchScreen.storyboard in Resources */, 270 | 65B16B9018BC16F6003AA819 /* Images.xcassets in Resources */, 271 | 65B16BBA18BC1826003AA819 /* CLTokenInputViewController.xib in Resources */, 272 | ); 273 | runOnlyForDeploymentPostprocessing = 0; 274 | }; 275 | 65B16B9318BC16F7003AA819 /* Resources */ = { 276 | isa = PBXResourcesBuildPhase; 277 | buildActionMask = 2147483647; 278 | files = ( 279 | 65B16BBB18BC1826003AA819 /* CLTokenInputViewController.xib in Resources */, 280 | 65B16BA118BC16F7003AA819 /* InfoPlist.strings in Resources */, 281 | ); 282 | runOnlyForDeploymentPostprocessing = 0; 283 | }; 284 | /* End PBXResourcesBuildPhase section */ 285 | 286 | /* Begin PBXSourcesBuildPhase section */ 287 | 65B16B7618BC16F6003AA819 /* Sources */ = { 288 | isa = PBXSourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | 65B16BBE18BC1D9E003AA819 /* CLBackspaceDetectingTextField.m in Sources */, 292 | 65B16BAF18BC17D2003AA819 /* CLTokenInputView.m in Sources */, 293 | 65B16BB818BC1826003AA819 /* CLTokenInputViewController.m in Sources */, 294 | 65B16B8A18BC16F6003AA819 /* main.m in Sources */, 295 | 65B16B8E18BC16F6003AA819 /* CLAppDelegate.m in Sources */, 296 | 65B16BB318BC17EC003AA819 /* CLTokenView.m in Sources */, 297 | 65B16BC218BC26C9003AA819 /* CLToken.m in Sources */, 298 | ); 299 | runOnlyForDeploymentPostprocessing = 0; 300 | }; 301 | 65B16B9118BC16F7003AA819 /* Sources */ = { 302 | isa = PBXSourcesBuildPhase; 303 | buildActionMask = 2147483647; 304 | files = ( 305 | 65B16BB018BC17D2003AA819 /* CLTokenInputView.m in Sources */, 306 | 65B16BA318BC16F7003AA819 /* CLTokenInputViewTests.m in Sources */, 307 | 65B16BC318BC26C9003AA819 /* CLToken.m in Sources */, 308 | 65B16BB918BC1826003AA819 /* CLTokenInputViewController.m in Sources */, 309 | 65B16BBF18BC1D9E003AA819 /* CLBackspaceDetectingTextField.m in Sources */, 310 | 65B16BB418BC17EC003AA819 /* CLTokenView.m in Sources */, 311 | ); 312 | runOnlyForDeploymentPostprocessing = 0; 313 | }; 314 | /* End PBXSourcesBuildPhase section */ 315 | 316 | /* Begin PBXTargetDependency section */ 317 | 65B16B9B18BC16F7003AA819 /* PBXTargetDependency */ = { 318 | isa = PBXTargetDependency; 319 | target = 65B16B7918BC16F6003AA819 /* CLTokenInputView */; 320 | targetProxy = 65B16B9A18BC16F7003AA819 /* PBXContainerItemProxy */; 321 | }; 322 | /* End PBXTargetDependency section */ 323 | 324 | /* Begin PBXVariantGroup section */ 325 | 65B16B8618BC16F6003AA819 /* InfoPlist.strings */ = { 326 | isa = PBXVariantGroup; 327 | children = ( 328 | 65B16B8718BC16F6003AA819 /* en */, 329 | ); 330 | name = InfoPlist.strings; 331 | sourceTree = ""; 332 | }; 333 | 65B16B9F18BC16F7003AA819 /* InfoPlist.strings */ = { 334 | isa = PBXVariantGroup; 335 | children = ( 336 | 65B16BA018BC16F7003AA819 /* en */, 337 | ); 338 | name = InfoPlist.strings; 339 | sourceTree = ""; 340 | }; 341 | /* End PBXVariantGroup section */ 342 | 343 | /* Begin XCBuildConfiguration section */ 344 | 65B16BA418BC16F7003AA819 /* Debug */ = { 345 | isa = XCBuildConfiguration; 346 | buildSettings = { 347 | ALWAYS_SEARCH_USER_PATHS = NO; 348 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 349 | CLANG_CXX_LIBRARY = "libc++"; 350 | CLANG_ENABLE_MODULES = YES; 351 | CLANG_ENABLE_OBJC_ARC = YES; 352 | CLANG_WARN_BOOL_CONVERSION = YES; 353 | CLANG_WARN_CONSTANT_CONVERSION = YES; 354 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 355 | CLANG_WARN_EMPTY_BODY = YES; 356 | CLANG_WARN_ENUM_CONVERSION = YES; 357 | CLANG_WARN_INT_CONVERSION = YES; 358 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 359 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 360 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 361 | COPY_PHASE_STRIP = NO; 362 | ENABLE_TESTABILITY = YES; 363 | GCC_C_LANGUAGE_STANDARD = gnu99; 364 | GCC_DYNAMIC_NO_PIC = NO; 365 | GCC_OPTIMIZATION_LEVEL = 0; 366 | GCC_PREPROCESSOR_DEFINITIONS = ( 367 | "DEBUG=1", 368 | "$(inherited)", 369 | ); 370 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 371 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 372 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 373 | GCC_WARN_UNDECLARED_SELECTOR = YES; 374 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 375 | GCC_WARN_UNUSED_FUNCTION = YES; 376 | GCC_WARN_UNUSED_VARIABLE = YES; 377 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 378 | ONLY_ACTIVE_ARCH = YES; 379 | SDKROOT = iphoneos; 380 | }; 381 | name = Debug; 382 | }; 383 | 65B16BA518BC16F7003AA819 /* Release */ = { 384 | isa = XCBuildConfiguration; 385 | buildSettings = { 386 | ALWAYS_SEARCH_USER_PATHS = NO; 387 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 388 | CLANG_CXX_LIBRARY = "libc++"; 389 | CLANG_ENABLE_MODULES = YES; 390 | CLANG_ENABLE_OBJC_ARC = YES; 391 | CLANG_WARN_BOOL_CONVERSION = YES; 392 | CLANG_WARN_CONSTANT_CONVERSION = YES; 393 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 394 | CLANG_WARN_EMPTY_BODY = YES; 395 | CLANG_WARN_ENUM_CONVERSION = YES; 396 | CLANG_WARN_INT_CONVERSION = YES; 397 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 398 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 399 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 400 | COPY_PHASE_STRIP = YES; 401 | ENABLE_NS_ASSERTIONS = NO; 402 | GCC_C_LANGUAGE_STANDARD = gnu99; 403 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 404 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 405 | GCC_WARN_UNDECLARED_SELECTOR = YES; 406 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 407 | GCC_WARN_UNUSED_FUNCTION = YES; 408 | GCC_WARN_UNUSED_VARIABLE = YES; 409 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 410 | SDKROOT = iphoneos; 411 | VALIDATE_PRODUCT = YES; 412 | }; 413 | name = Release; 414 | }; 415 | 65B16BA718BC16F7003AA819 /* Debug */ = { 416 | isa = XCBuildConfiguration; 417 | buildSettings = { 418 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 419 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 420 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 421 | GCC_PREFIX_HEADER = "CLTokenInputView/CLTokenInputView-Prefix.pch"; 422 | INFOPLIST_FILE = "CLTokenInputView/CLTokenInputView-Info.plist"; 423 | IPHONEOS_DEPLOYMENT_TARGET = 6.0; 424 | PRODUCT_BUNDLE_IDENTIFIER = "com.getcluster.${PRODUCT_NAME:rfc1034identifier}"; 425 | PRODUCT_NAME = "$(TARGET_NAME)"; 426 | WRAPPER_EXTENSION = app; 427 | }; 428 | name = Debug; 429 | }; 430 | 65B16BA818BC16F7003AA819 /* Release */ = { 431 | isa = XCBuildConfiguration; 432 | buildSettings = { 433 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 434 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 435 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 436 | GCC_PREFIX_HEADER = "CLTokenInputView/CLTokenInputView-Prefix.pch"; 437 | INFOPLIST_FILE = "CLTokenInputView/CLTokenInputView-Info.plist"; 438 | IPHONEOS_DEPLOYMENT_TARGET = 6.0; 439 | PRODUCT_BUNDLE_IDENTIFIER = "com.getcluster.${PRODUCT_NAME:rfc1034identifier}"; 440 | PRODUCT_NAME = "$(TARGET_NAME)"; 441 | WRAPPER_EXTENSION = app; 442 | }; 443 | name = Release; 444 | }; 445 | 65B16BAA18BC16F7003AA819 /* Debug */ = { 446 | isa = XCBuildConfiguration; 447 | buildSettings = { 448 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/CLTokenInputView.app/CLTokenInputView"; 449 | FRAMEWORK_SEARCH_PATHS = ( 450 | "$(SDKROOT)/Developer/Library/Frameworks", 451 | "$(inherited)", 452 | "$(DEVELOPER_FRAMEWORKS_DIR)", 453 | ); 454 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 455 | GCC_PREFIX_HEADER = "CLTokenInputView/CLTokenInputView-Prefix.pch"; 456 | GCC_PREPROCESSOR_DEFINITIONS = ( 457 | "DEBUG=1", 458 | "$(inherited)", 459 | ); 460 | INFOPLIST_FILE = "CLTokenInputViewTests/CLTokenInputViewTests-Info.plist"; 461 | PRODUCT_BUNDLE_IDENTIFIER = "com.getcluster.${PRODUCT_NAME:rfc1034identifier}"; 462 | PRODUCT_NAME = "$(TARGET_NAME)"; 463 | TEST_HOST = "$(BUNDLE_LOADER)"; 464 | WRAPPER_EXTENSION = xctest; 465 | }; 466 | name = Debug; 467 | }; 468 | 65B16BAB18BC16F7003AA819 /* Release */ = { 469 | isa = XCBuildConfiguration; 470 | buildSettings = { 471 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/CLTokenInputView.app/CLTokenInputView"; 472 | FRAMEWORK_SEARCH_PATHS = ( 473 | "$(SDKROOT)/Developer/Library/Frameworks", 474 | "$(inherited)", 475 | "$(DEVELOPER_FRAMEWORKS_DIR)", 476 | ); 477 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 478 | GCC_PREFIX_HEADER = "CLTokenInputView/CLTokenInputView-Prefix.pch"; 479 | INFOPLIST_FILE = "CLTokenInputViewTests/CLTokenInputViewTests-Info.plist"; 480 | PRODUCT_BUNDLE_IDENTIFIER = "com.getcluster.${PRODUCT_NAME:rfc1034identifier}"; 481 | PRODUCT_NAME = "$(TARGET_NAME)"; 482 | TEST_HOST = "$(BUNDLE_LOADER)"; 483 | WRAPPER_EXTENSION = xctest; 484 | }; 485 | name = Release; 486 | }; 487 | /* End XCBuildConfiguration section */ 488 | 489 | /* Begin XCConfigurationList section */ 490 | 65B16B7518BC16F6003AA819 /* Build configuration list for PBXProject "CLTokenInputView" */ = { 491 | isa = XCConfigurationList; 492 | buildConfigurations = ( 493 | 65B16BA418BC16F7003AA819 /* Debug */, 494 | 65B16BA518BC16F7003AA819 /* Release */, 495 | ); 496 | defaultConfigurationIsVisible = 0; 497 | defaultConfigurationName = Release; 498 | }; 499 | 65B16BA618BC16F7003AA819 /* Build configuration list for PBXNativeTarget "CLTokenInputView" */ = { 500 | isa = XCConfigurationList; 501 | buildConfigurations = ( 502 | 65B16BA718BC16F7003AA819 /* Debug */, 503 | 65B16BA818BC16F7003AA819 /* Release */, 504 | ); 505 | defaultConfigurationIsVisible = 0; 506 | defaultConfigurationName = Release; 507 | }; 508 | 65B16BA918BC16F7003AA819 /* Build configuration list for PBXNativeTarget "CLTokenInputViewTests" */ = { 509 | isa = XCConfigurationList; 510 | buildConfigurations = ( 511 | 65B16BAA18BC16F7003AA819 /* Debug */, 512 | 65B16BAB18BC16F7003AA819 /* Release */, 513 | ); 514 | defaultConfigurationIsVisible = 0; 515 | defaultConfigurationName = Release; 516 | }; 517 | /* End XCConfigurationList section */ 518 | }; 519 | rootObject = 65B16B7218BC16F6003AA819 /* Project object */; 520 | } 521 | -------------------------------------------------------------------------------- /CLTokenInputView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CLTokenInputView.xcodeproj/xcuserdata/rizwan.xcuserdatad/xcschemes/CLTokenInputView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /CLTokenInputView.xcodeproj/xcuserdata/rizwan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | CLTokenInputView.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 65B16B7918BC16F6003AA819 16 | 17 | primary 18 | 19 | 20 | 65B16B9418BC16F7003AA819 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /CLTokenInputView/CLAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLAppDelegate.h 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CLAppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /CLTokenInputView/CLAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // CLAppDelegate.m 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import "CLAppDelegate.h" 10 | 11 | #import "CLTokenInputViewController.h" 12 | 13 | @implementation CLAppDelegate 14 | 15 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 16 | { 17 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 18 | // Override point for customization after application launch. 19 | self.window.backgroundColor = [UIColor whiteColor]; 20 | 21 | CLTokenInputViewController *tokenVC = [[CLTokenInputViewController alloc] initWithNibName:nil bundle:nil]; 22 | UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:tokenVC]; 23 | self.window.rootViewController = nav; 24 | 25 | [self.window makeKeyAndVisible]; 26 | return YES; 27 | } 28 | 29 | - (void)applicationWillResignActive:(UIApplication *)application 30 | { 31 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 32 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 33 | } 34 | 35 | - (void)applicationDidEnterBackground:(UIApplication *)application 36 | { 37 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 38 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 39 | } 40 | 41 | - (void)applicationWillEnterForeground:(UIApplication *)application 42 | { 43 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 44 | } 45 | 46 | - (void)applicationDidBecomeActive:(UIApplication *)application 47 | { 48 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 49 | } 50 | 51 | - (void)applicationWillTerminate:(UIApplication *)application 52 | { 53 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputView-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputView-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #import 8 | 9 | #ifndef __IPHONE_3_0 10 | #warning "This project uses features only available in iOS SDK 3.0 and later." 11 | #endif 12 | 13 | #ifdef __OBJC__ 14 | #import 15 | #import 16 | #endif 17 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputView/CLBackspaceDetectingTextField.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLBackspaceDetectingTextField.h 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @class CLBackspaceDetectingTextField; 14 | @protocol CLBackspaceDetectingTextFieldDelegate 15 | 16 | - (void)textFieldDidDeleteBackwards:(UITextField *)textField; 17 | 18 | @end 19 | 20 | /** 21 | * CLBackspaceDetectingTextField is a very simple subclass 22 | * of UITextField that adds an extra delegate method to 23 | * notify whenever the backspace key is pressed. Without 24 | * this delegate method, it is not possible to detect 25 | * if the backspace key is pressed while the textfield is 26 | * empty. 27 | * 28 | * @since v1.0 29 | */ 30 | @interface CLBackspaceDetectingTextField : UITextField 31 | 32 | @property (weak, nonatomic, nullable) NSObject *delegate; 33 | 34 | @end 35 | 36 | NS_ASSUME_NONNULL_END 37 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputView/CLBackspaceDetectingTextField.m: -------------------------------------------------------------------------------- 1 | // 2 | // CLBackspaceDetectingTextField.m 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import "CLBackspaceDetectingTextField.h" 10 | 11 | @implementation CLBackspaceDetectingTextField 12 | 13 | @dynamic delegate; 14 | 15 | - (id)initWithFrame:(CGRect)frame 16 | { 17 | self = [super initWithFrame:frame]; 18 | if (self) { 19 | // Initialization code 20 | } 21 | return self; 22 | } 23 | 24 | // Listen for the deleteBackward method from UIKeyInput protocol 25 | - (void)deleteBackward 26 | { 27 | if ([self.delegate respondsToSelector:@selector(textFieldDidDeleteBackwards:)]) { 28 | [self.delegate textFieldDidDeleteBackwards:self]; 29 | } 30 | // Call super afterwards, so the -text property will return text 31 | // prior to the delete 32 | [super deleteBackward]; 33 | } 34 | 35 | // On iOS 8.0, deleteBackward is not called anymore, so according to: 36 | // http://stackoverflow.com/a/25862878/9849 37 | // This method override should work 38 | - (BOOL)keyboardInputShouldDelete:(UITextField *)textField { 39 | BOOL shouldDelete = YES; 40 | 41 | if ([UITextField instancesRespondToSelector:_cmd]) { 42 | BOOL (*keyboardInputShouldDelete)(id, SEL, UITextField *) = (BOOL (*)(id, SEL, UITextField *))[UITextField instanceMethodForSelector:_cmd]; 43 | 44 | if (keyboardInputShouldDelete) { 45 | shouldDelete = keyboardInputShouldDelete(self, _cmd, textField); 46 | } 47 | } 48 | 49 | if (![textField.text length] && [[[UIDevice currentDevice] systemVersion] intValue] >= 8) { 50 | [self deleteBackward]; 51 | } 52 | 53 | return shouldDelete; 54 | } 55 | 56 | // Override the delegate to ensure our own delegate subclass gets set 57 | - (void)setDelegate:(NSObject *)delegate 58 | { 59 | [super setDelegate:delegate]; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputView/CLToken.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLToken.h 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /** 14 | * This is a high level object that is provided to the 15 | * CLTokenInputView when tokens should be added/removed 16 | */ 17 | @interface CLToken : NSObject 18 | 19 | /** The text to display in the token view */ 20 | @property (copy, nonatomic) NSString *displayText; 21 | /** Used for storing anything that would be useful later on */ 22 | @property (strong, nonatomic, nullable) NSObject *context; 23 | 24 | 25 | - (id)initWithDisplayText:(NSString *)displayText context:(nullable NSObject *)context; 26 | 27 | @end 28 | 29 | NS_ASSUME_NONNULL_END 30 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputView/CLToken.m: -------------------------------------------------------------------------------- 1 | // 2 | // CLToken.m 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import "CLToken.h" 10 | 11 | @implementation CLToken 12 | 13 | - (id)initWithDisplayText:(NSString *)displayText context:(NSObject *)context 14 | { 15 | self = [super init]; 16 | if (self) { 17 | self.displayText = displayText; 18 | self.context = context; 19 | } 20 | return self; 21 | } 22 | 23 | - (BOOL)isEqual:(id)object 24 | { 25 | if (self == object) { 26 | return YES; 27 | } 28 | if (![object isKindOfClass:[CLToken class]]) { 29 | return NO; 30 | } 31 | 32 | CLToken *otherObject = (CLToken *)object; 33 | if ([otherObject.displayText isEqualToString:self.displayText] && 34 | [otherObject.context isEqual:self.context]) { 35 | return YES; 36 | } 37 | return NO; 38 | } 39 | 40 | - (NSUInteger)hash 41 | { 42 | return self.displayText.hash + self.context.hash; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputView/CLTokenInputView.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLTokenInputView.h 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "CLToken.h" 12 | 13 | #if __has_feature(objc_generics) 14 | #define CL_GENERIC_ARRAY(type) NSArray 15 | #define CL_GENERIC_MUTABLE_ARRAY(type) NSMutableArray 16 | #define CL_GENERIC_SET(type) NSSet 17 | #define CL_GENERIC_MUTABLE_SET(type) NSMutableSet 18 | #else 19 | #define CL_GENERIC_ARRAY(type) NSArray 20 | #define CL_GENERIC_MUTABLE_ARRAY(type) NSMutableArray 21 | #define CL_GENERIC_SET(type) NSSet 22 | #define CL_GENERIC_MUTABLE_SET(type) NSMutableSet 23 | #endif 24 | 25 | NS_ASSUME_NONNULL_BEGIN 26 | 27 | @class CLTokenInputView; 28 | @protocol CLTokenInputViewDelegate 29 | 30 | @optional 31 | 32 | /** 33 | * Called when the text field begins editing 34 | */ 35 | - (void)tokenInputViewDidEndEditing:(CLTokenInputView *)view; 36 | 37 | /** 38 | * Called when the text field ends editing 39 | */ 40 | - (void)tokenInputViewDidBeginEditing:(CLTokenInputView *)view; 41 | 42 | /** 43 | * Called when the text field should return 44 | */ 45 | - (BOOL)tokenInputViewShouldReturn:(CLTokenInputView *)view; 46 | 47 | /** 48 | * Called when the text field text has changed. You should update your autocompleting UI based on the text supplied. 49 | */ 50 | - (void)tokenInputView:(CLTokenInputView *)view didChangeText:(nullable NSString *)text; 51 | /** 52 | * Called when a token has been added. You should use this opportunity to update your local list of selected items. 53 | */ 54 | - (void)tokenInputView:(CLTokenInputView *)view didAddToken:(CLToken *)token; 55 | /** 56 | * Called when a token has been removed. You should use this opportunity to update your local list of selected items. 57 | */ 58 | - (void)tokenInputView:(CLTokenInputView *)view didRemoveToken:(CLToken *)token; 59 | /** 60 | * Called when the user attempts to press the Return key with text partially typed. 61 | * @return A CLToken for a match (typically the first item in the matching results), 62 | * or nil if the text shouldn't be accepted. 63 | */ 64 | - (nullable CLToken *)tokenInputView:(CLTokenInputView *)view tokenForText:(NSString *)text; 65 | /** 66 | * Called when the view has updated its own height. If you are 67 | * not using Autolayout, you should use this method to update the 68 | * frames to make sure the token view still fits. 69 | */ 70 | - (void)tokenInputView:(CLTokenInputView *)view didChangeHeightTo:(CGFloat)height; 71 | 72 | @end 73 | 74 | @interface CLTokenInputView : UIView 75 | 76 | @property (weak, nonatomic, nullable) IBOutlet NSObject *delegate; 77 | /** An optional view that shows up presumably on the first line */ 78 | @property (strong, nonatomic, nullable) UIView *fieldView; 79 | /** Option text which can be displayed before the first line (e.g. "To:") */ 80 | @property (copy, nonatomic, nullable) IBInspectable NSString *fieldName; 81 | /** Color of optional */ 82 | @property (strong, nonatomic, nullable) IBInspectable UIColor *fieldColor; 83 | @property (copy, nonatomic, nullable) IBInspectable NSString *placeholderText; 84 | @property (strong, nonatomic, nullable) UIView *accessoryView; 85 | @property (assign, nonatomic) IBInspectable UIKeyboardType keyboardType; 86 | @property (assign, nonatomic) IBInspectable UITextAutocapitalizationType autocapitalizationType; 87 | @property (assign, nonatomic) IBInspectable UITextAutocorrectionType autocorrectionType; 88 | @property (assign, nonatomic) IBInspectable UIKeyboardAppearance keyboardAppearance; 89 | /** 90 | * Optional additional characters to trigger the tokenization process (and call the delegate 91 | * with `tokenInputView:tokenForText:` 92 | * @discussion By default this array is empty, as only the Return key will trigger tokenization 93 | * however, if you would like to trigger tokenization with additional characters (such as a comma, 94 | * or as a space), you can supply the list here. 95 | */ 96 | @property (copy, nonatomic) CL_GENERIC_SET(NSString *) *tokenizationCharacters; 97 | @property (assign, nonatomic) IBInspectable BOOL drawBottomBorder; 98 | 99 | @property (readonly, nonatomic) CL_GENERIC_ARRAY(CLToken *) *allTokens; 100 | @property (readonly, nonatomic, getter = isEditing) BOOL editing; 101 | @property (readonly, nonatomic) CGFloat textFieldDisplayOffset; 102 | @property (copy, nonatomic, nullable) NSString *text; 103 | 104 | - (void)addToken:(CLToken *)token; 105 | - (void)removeToken:(CLToken *)token; 106 | - (nullable CLToken *)tokenizeTextfieldText; 107 | 108 | // Editing 109 | - (void)beginEditing; 110 | - (void)endEditing; 111 | 112 | @end 113 | 114 | NS_ASSUME_NONNULL_END 115 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputView/CLTokenInputView.m: -------------------------------------------------------------------------------- 1 | // 2 | // CLTokenInputView.m 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import "CLTokenInputView.h" 10 | 11 | #import "CLBackspaceDetectingTextField.h" 12 | #import "CLTokenView.h" 13 | 14 | static CGFloat const HSPACE = 0.0; 15 | static CGFloat const TEXT_FIELD_HSPACE = 4.0; // Note: Same as CLTokenView.PADDING_X 16 | static CGFloat const VSPACE = 4.0; 17 | static CGFloat const MINIMUM_TEXTFIELD_WIDTH = 56.0; 18 | static CGFloat const PADDING_TOP = 10.0; 19 | static CGFloat const PADDING_BOTTOM = 10.0; 20 | static CGFloat const PADDING_LEFT = 8.0; 21 | static CGFloat const PADDING_RIGHT = 16.0; 22 | static CGFloat const STANDARD_ROW_HEIGHT = 25.0; 23 | 24 | static CGFloat const FIELD_MARGIN_X = 4.0; // Note: Same as CLTokenView.PADDING_X 25 | 26 | @interface CLTokenInputView () 27 | 28 | @property (strong, nonatomic) CL_GENERIC_MUTABLE_ARRAY(CLToken *) *tokens; 29 | @property (strong, nonatomic) CL_GENERIC_MUTABLE_ARRAY(CLTokenView *) *tokenViews; 30 | @property (strong, nonatomic) CLBackspaceDetectingTextField *textField; 31 | @property (strong, nonatomic) UILabel *fieldLabel; 32 | 33 | 34 | @property (assign, nonatomic) CGFloat intrinsicContentHeight; 35 | @property (assign, nonatomic) CGFloat additionalTextFieldYOffset; 36 | 37 | @end 38 | 39 | @implementation CLTokenInputView 40 | 41 | - (void)commonInit 42 | { 43 | self.textField = [[CLBackspaceDetectingTextField alloc] initWithFrame:self.bounds]; 44 | self.textField.backgroundColor = [UIColor clearColor]; 45 | self.textField.keyboardType = UIKeyboardTypeEmailAddress; 46 | self.textField.autocorrectionType = UITextAutocorrectionTypeNo; 47 | self.textField.autocapitalizationType = UITextAutocapitalizationTypeNone; 48 | self.textField.delegate = self; 49 | self.additionalTextFieldYOffset = 0.0; 50 | if (![self.textField respondsToSelector:@selector(defaultTextAttributes)]) { 51 | self.additionalTextFieldYOffset = 1.5; 52 | } 53 | [self.textField addTarget:self 54 | action:@selector(onTextFieldDidChange:) 55 | forControlEvents:UIControlEventEditingChanged]; 56 | [self addSubview:self.textField]; 57 | 58 | self.tokens = [NSMutableArray arrayWithCapacity:20]; 59 | self.tokenViews = [NSMutableArray arrayWithCapacity:20]; 60 | 61 | self.fieldColor = [UIColor lightGrayColor]; 62 | 63 | self.fieldLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 64 | // NOTE: Explicitly not setting a font for the field label 65 | self.fieldLabel.textColor = self.fieldColor; 66 | [self addSubview:self.fieldLabel]; 67 | self.fieldLabel.hidden = YES; 68 | 69 | self.intrinsicContentHeight = STANDARD_ROW_HEIGHT; 70 | [self repositionViews]; 71 | } 72 | 73 | - (id)initWithFrame:(CGRect)frame 74 | { 75 | self = [super initWithFrame:frame]; 76 | if (self) { 77 | [self commonInit]; 78 | } 79 | return self; 80 | } 81 | 82 | - (id)initWithCoder:(NSCoder *)aDecoder 83 | { 84 | self = [super initWithCoder:aDecoder]; 85 | if (self) { 86 | [self commonInit]; 87 | } 88 | return self; 89 | } 90 | 91 | - (CGSize)intrinsicContentSize 92 | { 93 | return CGSizeMake(UIViewNoIntrinsicMetric, MAX(45, self.intrinsicContentHeight)); 94 | } 95 | 96 | 97 | #pragma mark - Tint color 98 | 99 | 100 | - (void)tintColorDidChange 101 | { 102 | for (UIView *tokenView in self.tokenViews) { 103 | tokenView.tintColor = self.tintColor; 104 | } 105 | } 106 | 107 | 108 | #pragma mark - Adding / Removing Tokens 109 | 110 | - (void)addToken:(CLToken *)token 111 | { 112 | if ([self.tokens containsObject:token]) { 113 | return; 114 | } 115 | 116 | [self.tokens addObject:token]; 117 | CLTokenView *tokenView = [[CLTokenView alloc] initWithToken:token font:self.textField.font]; 118 | if ([self respondsToSelector:@selector(tintColor)]) { 119 | tokenView.tintColor = self.tintColor; 120 | } 121 | tokenView.delegate = self; 122 | CGSize intrinsicSize = tokenView.intrinsicContentSize; 123 | tokenView.frame = CGRectMake(0, 0, intrinsicSize.width, intrinsicSize.height); 124 | [self.tokenViews addObject:tokenView]; 125 | [self addSubview:tokenView]; 126 | self.textField.text = @""; 127 | if ([self.delegate respondsToSelector:@selector(tokenInputView:didAddToken:)]) { 128 | [self.delegate tokenInputView:self didAddToken:token]; 129 | } 130 | 131 | // Clearing text programmatically doesn't call this automatically 132 | [self onTextFieldDidChange:self.textField]; 133 | 134 | [self updatePlaceholderTextVisibility]; 135 | [self repositionViews]; 136 | } 137 | 138 | - (void)removeToken:(CLToken *)token 139 | { 140 | NSInteger index = [self.tokens indexOfObject:token]; 141 | if (index == NSNotFound) { 142 | return; 143 | } 144 | [self removeTokenAtIndex:index]; 145 | } 146 | 147 | - (void)removeTokenAtIndex:(NSInteger)index 148 | { 149 | if (index == NSNotFound) { 150 | return; 151 | } 152 | CLTokenView *tokenView = self.tokenViews[index]; 153 | [tokenView removeFromSuperview]; 154 | [self.tokenViews removeObjectAtIndex:index]; 155 | CLToken *removedToken = self.tokens[index]; 156 | [self.tokens removeObjectAtIndex:index]; 157 | if ([self.delegate respondsToSelector:@selector(tokenInputView:didRemoveToken:)]) { 158 | [self.delegate tokenInputView:self didRemoveToken:removedToken]; 159 | } 160 | [self updatePlaceholderTextVisibility]; 161 | [self repositionViews]; 162 | } 163 | 164 | - (NSArray *)allTokens 165 | { 166 | return [self.tokens copy]; 167 | } 168 | 169 | - (CLToken *)tokenizeTextfieldText 170 | { 171 | CLToken *token = nil; 172 | NSString *text = self.textField.text; 173 | if (text.length > 0 && 174 | [self.delegate respondsToSelector:@selector(tokenInputView:tokenForText:)]) { 175 | token = [self.delegate tokenInputView:self tokenForText:text]; 176 | if (token != nil) { 177 | [self addToken:token]; 178 | self.textField.text = @""; 179 | [self onTextFieldDidChange:self.textField]; 180 | } 181 | } 182 | return token; 183 | } 184 | 185 | 186 | #pragma mark - Updating/Repositioning Views 187 | 188 | - (void)repositionViews 189 | { 190 | CGRect bounds = self.bounds; 191 | CGFloat rightBoundary = CGRectGetWidth(bounds) - PADDING_RIGHT; 192 | CGFloat firstLineRightBoundary = rightBoundary; 193 | 194 | CGFloat curX = PADDING_LEFT; 195 | CGFloat curY = PADDING_TOP; 196 | CGFloat totalHeight = STANDARD_ROW_HEIGHT; 197 | BOOL isOnFirstLine = YES; 198 | 199 | // Position field view (if set) 200 | if (self.fieldView) { 201 | CGRect fieldViewRect = self.fieldView.frame; 202 | fieldViewRect.origin.x = curX + FIELD_MARGIN_X; 203 | fieldViewRect.origin.y = curY + ((STANDARD_ROW_HEIGHT - CGRectGetHeight(fieldViewRect))/2.0); 204 | self.fieldView.frame = fieldViewRect; 205 | 206 | curX = CGRectGetMaxX(fieldViewRect) + FIELD_MARGIN_X; 207 | } 208 | 209 | // Position field label (if field name is set) 210 | if (!self.fieldLabel.hidden) { 211 | CGSize labelSize = self.fieldLabel.intrinsicContentSize; 212 | CGRect fieldLabelRect = CGRectZero; 213 | fieldLabelRect.size = labelSize; 214 | fieldLabelRect.origin.x = curX + FIELD_MARGIN_X; 215 | fieldLabelRect.origin.y = curY + ((STANDARD_ROW_HEIGHT-CGRectGetHeight(fieldLabelRect))/2.0); 216 | self.fieldLabel.frame = fieldLabelRect; 217 | 218 | curX = CGRectGetMaxX(fieldLabelRect) + FIELD_MARGIN_X; 219 | } 220 | 221 | // Position accessory view (if set) 222 | if (self.accessoryView) { 223 | CGRect accessoryRect = self.accessoryView.frame; 224 | accessoryRect.origin.x = CGRectGetWidth(bounds) - PADDING_RIGHT - CGRectGetWidth(accessoryRect); 225 | accessoryRect.origin.y = curY; 226 | self.accessoryView.frame = accessoryRect; 227 | 228 | firstLineRightBoundary = CGRectGetMinX(accessoryRect) - HSPACE; 229 | } 230 | 231 | // Position token views 232 | CGRect tokenRect = CGRectNull; 233 | for (UIView *tokenView in self.tokenViews) { 234 | tokenRect = tokenView.frame; 235 | 236 | CGFloat tokenBoundary = isOnFirstLine ? firstLineRightBoundary : rightBoundary; 237 | if (curX + CGRectGetWidth(tokenRect) > tokenBoundary) { 238 | // Need a new line 239 | curX = PADDING_LEFT; 240 | curY += STANDARD_ROW_HEIGHT+VSPACE; 241 | totalHeight += STANDARD_ROW_HEIGHT; 242 | isOnFirstLine = NO; 243 | } 244 | 245 | tokenRect.origin.x = curX; 246 | // Center our tokenView vertically within STANDARD_ROW_HEIGHT 247 | tokenRect.origin.y = curY + ((STANDARD_ROW_HEIGHT-CGRectGetHeight(tokenRect))/2.0); 248 | tokenView.frame = tokenRect; 249 | 250 | curX = CGRectGetMaxX(tokenRect) + HSPACE; 251 | } 252 | 253 | // Always indent textfield by a little bit 254 | curX += TEXT_FIELD_HSPACE; 255 | CGFloat textBoundary = isOnFirstLine ? firstLineRightBoundary : rightBoundary; 256 | CGFloat availableWidthForTextField = textBoundary - curX; 257 | if (availableWidthForTextField < MINIMUM_TEXTFIELD_WIDTH) { 258 | isOnFirstLine = NO; 259 | // If in the future we add more UI elements below the tokens, 260 | // isOnFirstLine will be useful, and this calculation is important. 261 | // So leaving it set here, and marking the warning to ignore it 262 | #pragma unused(isOnFirstLine) 263 | curX = PADDING_LEFT + TEXT_FIELD_HSPACE; 264 | curY += STANDARD_ROW_HEIGHT+VSPACE; 265 | totalHeight += STANDARD_ROW_HEIGHT; 266 | // Adjust the width 267 | availableWidthForTextField = rightBoundary - curX; 268 | } 269 | 270 | CGRect textFieldRect = self.textField.frame; 271 | textFieldRect.origin.x = curX; 272 | textFieldRect.origin.y = curY + self.additionalTextFieldYOffset; 273 | textFieldRect.size.width = availableWidthForTextField; 274 | textFieldRect.size.height = STANDARD_ROW_HEIGHT; 275 | self.textField.frame = textFieldRect; 276 | 277 | CGFloat oldContentHeight = self.intrinsicContentHeight; 278 | self.intrinsicContentHeight = MAX(totalHeight, CGRectGetMaxY(textFieldRect)+PADDING_BOTTOM); 279 | [self invalidateIntrinsicContentSize]; 280 | 281 | if (oldContentHeight != self.intrinsicContentHeight) { 282 | if ([self.delegate respondsToSelector:@selector(tokenInputView:didChangeHeightTo:)]) { 283 | [self.delegate tokenInputView:self didChangeHeightTo:self.intrinsicContentSize.height]; 284 | } 285 | } 286 | [self setNeedsDisplay]; 287 | } 288 | 289 | - (void)updatePlaceholderTextVisibility 290 | { 291 | if (self.tokens.count > 0) { 292 | self.textField.placeholder = nil; 293 | } else { 294 | self.textField.placeholder = self.placeholderText; 295 | } 296 | } 297 | 298 | 299 | - (void)layoutSubviews 300 | { 301 | [super layoutSubviews]; 302 | [self repositionViews]; 303 | } 304 | 305 | 306 | #pragma mark - CLBackspaceDetectingTextFieldDelegate 307 | 308 | - (void)textFieldDidDeleteBackwards:(UITextField *)textField 309 | { 310 | // Delay selecting the next token slightly, so that on iOS 8 311 | // the deleteBackward on CLTokenView is not called immediately, 312 | // causing a double-delete 313 | dispatch_async(dispatch_get_main_queue(), ^{ 314 | if (textField.text.length == 0) { 315 | CLTokenView *tokenView = self.tokenViews.lastObject; 316 | if (tokenView) { 317 | [self selectTokenView:tokenView animated:YES]; 318 | [self.textField resignFirstResponder]; 319 | } 320 | } 321 | }); 322 | } 323 | 324 | 325 | #pragma mark - UITextFieldDelegate 326 | 327 | - (void)textFieldDidBeginEditing:(UITextField *)textField 328 | { 329 | if ([self.delegate respondsToSelector:@selector(tokenInputViewDidBeginEditing:)]) { 330 | [self.delegate tokenInputViewDidBeginEditing:self]; 331 | } 332 | self.tokenViews.lastObject.hideUnselectedComma = NO; 333 | [self unselectAllTokenViewsAnimated:YES]; 334 | } 335 | 336 | - (void)textFieldDidEndEditing:(UITextField *)textField 337 | { 338 | if ([self.delegate respondsToSelector:@selector(tokenInputViewDidEndEditing:)]) { 339 | [self.delegate tokenInputViewDidEndEditing:self]; 340 | } 341 | self.tokenViews.lastObject.hideUnselectedComma = YES; 342 | } 343 | 344 | - (BOOL)textFieldShouldReturn:(UITextField *)textField 345 | { 346 | [self tokenizeTextfieldText]; 347 | BOOL shouldDoDefaultBehavior = NO; 348 | if ([self.delegate respondsToSelector:@selector(tokenInputViewShouldReturn:)]) { 349 | shouldDoDefaultBehavior = [self.delegate tokenInputViewShouldReturn:self]; 350 | } 351 | return shouldDoDefaultBehavior; 352 | } 353 | 354 | - (BOOL) textField:(UITextField *)textField 355 | shouldChangeCharactersInRange:(NSRange)range 356 | replacementString:(NSString *)string 357 | { 358 | if (string.length > 0 && [self.tokenizationCharacters member:string]) { 359 | [self tokenizeTextfieldText]; 360 | // Never allow the change if it matches at token 361 | return NO; 362 | } 363 | return YES; 364 | } 365 | 366 | 367 | #pragma mark - Text Field Changes 368 | 369 | - (void)onTextFieldDidChange:(id)sender 370 | { 371 | if ([self.delegate respondsToSelector:@selector(tokenInputView:didChangeText:)]) { 372 | [self.delegate tokenInputView:self didChangeText:self.textField.text]; 373 | } 374 | } 375 | 376 | 377 | #pragma mark - Text Field Customization 378 | 379 | - (void)setKeyboardType:(UIKeyboardType)keyboardType 380 | { 381 | _keyboardType = keyboardType; 382 | self.textField.keyboardType = _keyboardType; 383 | } 384 | 385 | - (void)setAutocapitalizationType:(UITextAutocapitalizationType)autocapitalizationType 386 | { 387 | _autocapitalizationType = autocapitalizationType; 388 | self.textField.autocapitalizationType = _autocapitalizationType; 389 | } 390 | 391 | - (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType 392 | { 393 | _autocorrectionType = autocorrectionType; 394 | self.textField.autocorrectionType = _autocorrectionType; 395 | } 396 | 397 | - (void)setKeyboardAppearance:(UIKeyboardAppearance)keyboardAppearance 398 | { 399 | _keyboardAppearance = keyboardAppearance; 400 | self.textField.keyboardAppearance = _keyboardAppearance; 401 | } 402 | 403 | 404 | #pragma mark - Measurements (text field offset, etc.) 405 | 406 | - (CGFloat)textFieldDisplayOffset 407 | { 408 | // Essentially the textfield's y with PADDING_TOP 409 | return CGRectGetMinY(self.textField.frame) - PADDING_TOP; 410 | } 411 | 412 | 413 | #pragma mark - Textfield text 414 | 415 | 416 | - (NSString *)text 417 | { 418 | return self.textField.text; 419 | } 420 | 421 | 422 | -(void) setText:(NSString*)text { 423 | self.textField.text = text; 424 | } 425 | 426 | #pragma mark - CLTokenViewDelegate 427 | 428 | - (void)tokenViewDidRequestDelete:(CLTokenView *)tokenView replaceWithText:(NSString *)replacementText 429 | { 430 | // First, refocus the text field 431 | [self.textField becomeFirstResponder]; 432 | if (replacementText.length > 0) { 433 | self.textField.text = replacementText; 434 | } 435 | // Then remove the view from our data 436 | NSInteger index = [self.tokenViews indexOfObject:tokenView]; 437 | if (index == NSNotFound) { 438 | return; 439 | } 440 | [self removeTokenAtIndex:index]; 441 | } 442 | 443 | - (void)tokenViewDidRequestSelection:(CLTokenView *)tokenView 444 | { 445 | [self selectTokenView:tokenView animated:YES]; 446 | } 447 | 448 | 449 | #pragma mark - Token selection 450 | 451 | - (void)selectTokenView:(CLTokenView *)tokenView animated:(BOOL)animated 452 | { 453 | [tokenView setSelected:YES animated:animated]; 454 | for (CLTokenView *otherTokenView in self.tokenViews) { 455 | if (otherTokenView != tokenView) { 456 | [otherTokenView setSelected:NO animated:animated]; 457 | } 458 | } 459 | } 460 | 461 | - (void)unselectAllTokenViewsAnimated:(BOOL)animated 462 | { 463 | for (CLTokenView *tokenView in self.tokenViews) { 464 | [tokenView setSelected:NO animated:animated]; 465 | } 466 | } 467 | 468 | 469 | #pragma mark - Editing 470 | 471 | - (BOOL)isEditing 472 | { 473 | return self.textField.editing; 474 | } 475 | 476 | 477 | - (void)beginEditing 478 | { 479 | [self.textField becomeFirstResponder]; 480 | [self unselectAllTokenViewsAnimated:NO]; 481 | } 482 | 483 | 484 | - (void)endEditing 485 | { 486 | // NOTE: We used to check if .isFirstResponder 487 | // and then resign first responder, but sometimes 488 | // we noticed that it would be the first responder, 489 | // but still return isFirstResponder=NO. So always 490 | // attempt to resign without checking. 491 | [self.textField resignFirstResponder]; 492 | } 493 | 494 | 495 | #pragma mark - (Optional Views) 496 | 497 | - (void)setFieldName:(NSString *)fieldName 498 | { 499 | if (_fieldName == fieldName) { 500 | return; 501 | } 502 | NSString *oldFieldName = _fieldName; 503 | _fieldName = fieldName; 504 | 505 | self.fieldLabel.text = _fieldName; 506 | [self.fieldLabel invalidateIntrinsicContentSize]; 507 | BOOL showField = (_fieldName.length > 0); 508 | self.fieldLabel.hidden = !showField; 509 | if (showField && !self.fieldLabel.superview) { 510 | [self addSubview:self.fieldLabel]; 511 | } else if (!showField && self.fieldLabel.superview) { 512 | [self.fieldLabel removeFromSuperview]; 513 | } 514 | 515 | if (oldFieldName == nil || ![oldFieldName isEqualToString:fieldName]) { 516 | [self repositionViews]; 517 | } 518 | } 519 | 520 | - (void)setFieldColor:(UIColor *)fieldColor { 521 | _fieldColor = fieldColor; 522 | self.fieldLabel.textColor = _fieldColor; 523 | } 524 | 525 | - (void)setFieldView:(UIView *)fieldView 526 | { 527 | if (_fieldView == fieldView) { 528 | return; 529 | } 530 | [_fieldView removeFromSuperview]; 531 | _fieldView = fieldView; 532 | if (_fieldView != nil) { 533 | [self addSubview:_fieldView]; 534 | } 535 | [self repositionViews]; 536 | } 537 | 538 | - (void)setPlaceholderText:(NSString *)placeholderText 539 | { 540 | if (_placeholderText == placeholderText) { 541 | return; 542 | } 543 | _placeholderText = placeholderText; 544 | [self updatePlaceholderTextVisibility]; 545 | } 546 | 547 | - (void)setAccessoryView:(UIView *)accessoryView 548 | { 549 | if (_accessoryView == accessoryView) { 550 | return; 551 | } 552 | [_accessoryView removeFromSuperview]; 553 | _accessoryView = accessoryView; 554 | 555 | if (_accessoryView != nil) { 556 | [self addSubview:_accessoryView]; 557 | } 558 | [self repositionViews]; 559 | } 560 | 561 | 562 | #pragma mark - Drawing 563 | 564 | - (void)setDrawBottomBorder:(BOOL)drawBottomBorder 565 | { 566 | if (_drawBottomBorder == drawBottomBorder) { 567 | return; 568 | } 569 | _drawBottomBorder = drawBottomBorder; 570 | [self setNeedsDisplay]; 571 | } 572 | 573 | 574 | // Only override drawRect: if you perform custom drawing. 575 | // An empty implementation adversely affects performance during animation. 576 | - (void)drawRect:(CGRect)rect 577 | { 578 | [super drawRect:rect]; 579 | if (self.drawBottomBorder) { 580 | 581 | CGContextRef context = UIGraphicsGetCurrentContext(); 582 | CGRect bounds = self.bounds; 583 | CGContextSetStrokeColorWithColor(context, [UIColor lightGrayColor].CGColor); 584 | CGContextSetLineWidth(context, 0.5); 585 | 586 | CGContextMoveToPoint(context, 0, bounds.size.height); 587 | CGContextAddLineToPoint(context, CGRectGetWidth(bounds), bounds.size.height); 588 | CGContextStrokePath(context); 589 | } 590 | } 591 | 592 | @end 593 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputView/CLTokenView.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLTokenView.h 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "CLToken.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @class CLTokenView; 16 | @protocol CLTokenViewDelegate 17 | 18 | @required 19 | - (void)tokenViewDidRequestDelete:(CLTokenView *)tokenView replaceWithText:(nullable NSString *)replacementText; 20 | - (void)tokenViewDidRequestSelection:(CLTokenView *)tokenView; 21 | 22 | @end 23 | 24 | 25 | @interface CLTokenView : UIView 26 | 27 | @property (weak, nonatomic, nullable) NSObject *delegate; 28 | @property (assign, nonatomic) BOOL selected; 29 | @property (assign, nonatomic) BOOL hideUnselectedComma; 30 | 31 | - (id)initWithToken:(CLToken *)token font:(nullable UIFont *)font; 32 | 33 | - (void)setSelected:(BOOL)selected animated:(BOOL)animated; 34 | 35 | // For iOS 6 compatibility, provide the setter tintColor 36 | - (void)setTintColor:(nullable UIColor *)tintColor; 37 | 38 | @end 39 | 40 | NS_ASSUME_NONNULL_END 41 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputView/CLTokenView.m: -------------------------------------------------------------------------------- 1 | // 2 | // CLTokenView.m 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import "CLTokenView.h" 10 | 11 | #import 12 | 13 | static CGFloat const PADDING_X = 4.0; 14 | static CGFloat const PADDING_Y = 2.0; 15 | 16 | static NSString *const UNSELECTED_LABEL_FORMAT = @"%@,"; 17 | static NSString *const UNSELECTED_LABEL_NO_COMMA_FORMAT = @"%@"; 18 | 19 | 20 | @interface CLTokenView () 21 | 22 | @property (strong, nonatomic) UIView *backgroundView; 23 | @property (strong, nonatomic) UILabel *label; 24 | 25 | @property (strong, nonatomic) UIView *selectedBackgroundView; 26 | @property (strong, nonatomic) UILabel *selectedLabel; 27 | 28 | @property (copy, nonatomic) NSString *displayText; 29 | 30 | @end 31 | 32 | @implementation CLTokenView 33 | 34 | - (id)initWithToken:(CLToken *)token font:(nullable UIFont *)font 35 | { 36 | self = [super initWithFrame:CGRectZero]; 37 | if (self) { 38 | UIColor *tintColor = [UIColor colorWithRed:0.0823 green:0.4941 blue:0.9843 alpha:1.0]; 39 | if ([self respondsToSelector:@selector(tintColor)]) { 40 | tintColor = self.tintColor; 41 | } 42 | self.label = [[UILabel alloc] initWithFrame:CGRectMake(PADDING_X, PADDING_Y, 0, 0)]; 43 | if (font) { 44 | self.label.font = font; 45 | } 46 | self.label.textColor = tintColor; 47 | self.label.backgroundColor = [UIColor clearColor]; 48 | [self addSubview:self.label]; 49 | 50 | self.selectedBackgroundView = [[UIView alloc] initWithFrame:CGRectZero]; 51 | self.selectedBackgroundView.backgroundColor = tintColor; 52 | self.selectedBackgroundView.layer.cornerRadius = 3.0; 53 | [self addSubview:self.selectedBackgroundView]; 54 | self.selectedBackgroundView.hidden = YES; 55 | 56 | self.selectedLabel = [[UILabel alloc] initWithFrame:CGRectMake(PADDING_X, PADDING_Y, 0, 0)]; 57 | self.selectedLabel.font = self.label.font; 58 | self.selectedLabel.textColor = [UIColor whiteColor]; 59 | self.selectedLabel.backgroundColor = [UIColor clearColor]; 60 | [self addSubview:self.selectedLabel]; 61 | self.selectedLabel.hidden = YES; 62 | 63 | self.displayText = token.displayText; 64 | 65 | self.hideUnselectedComma = NO; 66 | 67 | [self updateLabelAttributedText]; 68 | self.selectedLabel.text = token.displayText; 69 | 70 | // Listen for taps 71 | UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGestureRecognizer:)]; 72 | [self addGestureRecognizer:tapRecognizer]; 73 | 74 | [self setNeedsLayout]; 75 | 76 | } 77 | return self; 78 | } 79 | 80 | #pragma mark - Size Measurements 81 | 82 | - (CGSize)intrinsicContentSize 83 | { 84 | CGSize labelIntrinsicSize = self.selectedLabel.intrinsicContentSize; 85 | return CGSizeMake(labelIntrinsicSize.width+(2.0*PADDING_X), labelIntrinsicSize.height+(2.0*PADDING_Y)); 86 | } 87 | 88 | - (CGSize)sizeThatFits:(CGSize)size 89 | { 90 | CGSize fittingSize = CGSizeMake(size.width-(2.0*PADDING_X), size.height-(2.0*PADDING_Y)); 91 | CGSize labelSize = [self.selectedLabel sizeThatFits:fittingSize]; 92 | return CGSizeMake(labelSize.width+(2.0*PADDING_X), labelSize.height+(2.0*PADDING_Y)); 93 | } 94 | 95 | 96 | #pragma mark - Tinting 97 | 98 | 99 | - (void)setTintColor:(UIColor *)tintColor 100 | { 101 | if ([UIView instancesRespondToSelector:@selector(setTintColor:)]) { 102 | super.tintColor = tintColor; 103 | } 104 | self.label.textColor = tintColor; 105 | self.selectedBackgroundView.backgroundColor = tintColor; 106 | [self updateLabelAttributedText]; 107 | } 108 | 109 | 110 | #pragma mark - Hide Unselected Comma 111 | 112 | 113 | - (void)setHideUnselectedComma:(BOOL)hideUnselectedComma 114 | { 115 | if (_hideUnselectedComma == hideUnselectedComma) { 116 | return; 117 | } 118 | _hideUnselectedComma = hideUnselectedComma; 119 | [self updateLabelAttributedText]; 120 | } 121 | 122 | 123 | #pragma mark - Taps 124 | 125 | -(void)handleTapGestureRecognizer:(id)sender 126 | { 127 | [self.delegate tokenViewDidRequestSelection:self]; 128 | } 129 | 130 | 131 | #pragma mark - Selection 132 | 133 | - (void)setSelected:(BOOL)selected 134 | { 135 | [self setSelected:selected animated:NO]; 136 | } 137 | 138 | - (void)setSelected:(BOOL)selected animated:(BOOL)animated 139 | { 140 | if (_selected == selected) { 141 | return; 142 | } 143 | _selected = selected; 144 | 145 | if (selected && !self.isFirstResponder) { 146 | [self becomeFirstResponder]; 147 | } else if (!selected && self.isFirstResponder) { 148 | [self resignFirstResponder]; 149 | } 150 | CGFloat selectedAlpha = (_selected ? 1.0 : 0.0); 151 | if (animated) { 152 | if (_selected) { 153 | self.selectedBackgroundView.alpha = 0.0; 154 | self.selectedBackgroundView.hidden = NO; 155 | self.selectedLabel.alpha = 0.0; 156 | self.selectedLabel.hidden = NO; 157 | } 158 | [UIView animateWithDuration:0.25 animations:^{ 159 | self.selectedBackgroundView.alpha = selectedAlpha; 160 | self.selectedLabel.alpha = selectedAlpha; 161 | } completion:^(BOOL finished) { 162 | if (!_selected) { 163 | self.selectedBackgroundView.hidden = YES; 164 | self.selectedLabel.hidden = YES; 165 | } 166 | }]; 167 | } else { 168 | self.selectedBackgroundView.hidden = !_selected; 169 | self.selectedLabel.hidden = !_selected; 170 | } 171 | } 172 | 173 | 174 | #pragma mark - Attributed Text 175 | 176 | 177 | - (void)updateLabelAttributedText 178 | { 179 | // Configure for the token, unselected shows "[displayText]," and selected is "[displayText]" 180 | NSString *format = UNSELECTED_LABEL_FORMAT; 181 | if (self.hideUnselectedComma) { 182 | format = UNSELECTED_LABEL_NO_COMMA_FORMAT; 183 | } 184 | NSString *labelString = [NSString stringWithFormat:format, self.displayText]; 185 | NSMutableAttributedString *attrString = 186 | [[NSMutableAttributedString alloc] initWithString:labelString 187 | attributes:@{NSFontAttributeName : self.label.font, 188 | NSForegroundColorAttributeName : [UIColor lightGrayColor]}]; 189 | NSRange tintRange = [labelString rangeOfString:self.displayText]; 190 | // Make the name part the system tint color 191 | UIColor *tintColor = self.selectedBackgroundView.backgroundColor; 192 | if ([UIView instancesRespondToSelector:@selector(tintColor)]) { 193 | tintColor = self.tintColor; 194 | } 195 | [attrString setAttributes:@{NSForegroundColorAttributeName : tintColor} 196 | range:tintRange]; 197 | self.label.attributedText = attrString; 198 | } 199 | 200 | 201 | #pragma mark - Laying out 202 | 203 | - (void)layoutSubviews 204 | { 205 | [super layoutSubviews]; 206 | 207 | CGRect bounds = self.bounds; 208 | 209 | self.backgroundView.frame = bounds; 210 | self.selectedBackgroundView.frame = bounds; 211 | 212 | CGRect labelFrame = CGRectInset(bounds, PADDING_X, PADDING_Y); 213 | self.selectedLabel.frame = labelFrame; 214 | labelFrame.size.width += PADDING_X*2.0; 215 | self.label.frame = labelFrame; 216 | } 217 | 218 | /* 219 | // Only override drawRect: if you perform custom drawing. 220 | // An empty implementation adversely affects performance during animation. 221 | - (void)drawRect:(CGRect)rect 222 | { 223 | // Drawing code 224 | } 225 | */ 226 | 227 | 228 | #pragma mark - UIKeyInput protocol 229 | 230 | - (BOOL)hasText 231 | { 232 | return YES; 233 | } 234 | 235 | - (void)insertText:(NSString *)text 236 | { 237 | [self.delegate tokenViewDidRequestDelete:self replaceWithText:text]; 238 | } 239 | 240 | - (void)deleteBackward 241 | { 242 | [self.delegate tokenViewDidRequestDelete:self replaceWithText:nil]; 243 | } 244 | 245 | 246 | #pragma mark - UITextInputTraits protocol (inherited from UIKeyInput protocol) 247 | 248 | // Since a token isn't really meant to be "corrected" once created, disable autocorrect on it 249 | // See: https://github.com/clusterinc/CLTokenInputView/issues/2 250 | - (UITextAutocorrectionType)autocorrectionType 251 | { 252 | return UITextAutocorrectionTypeNo; 253 | } 254 | 255 | 256 | #pragma mark - First Responder (needed to capture keyboard) 257 | 258 | -(BOOL)canBecomeFirstResponder 259 | { 260 | return YES; 261 | } 262 | 263 | 264 | -(BOOL)resignFirstResponder 265 | { 266 | BOOL didResignFirstResponder = [super resignFirstResponder]; 267 | [self setSelected:NO animated:NO]; 268 | return didResignFirstResponder; 269 | } 270 | 271 | -(BOOL)becomeFirstResponder 272 | { 273 | BOOL didBecomeFirstResponder = [super becomeFirstResponder]; 274 | [self setSelected:YES animated:NO]; 275 | return didBecomeFirstResponder; 276 | } 277 | 278 | 279 | @end 280 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLTokenInputViewController.h 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "CLTokenInputView.h" 12 | 13 | @interface CLTokenInputViewController : UIViewController 14 | 15 | @property (strong, nonatomic) IBOutlet NSLayoutConstraint *tokenInputTopSpace; 16 | @property (strong, nonatomic) IBOutlet CLTokenInputView *tokenInputView; 17 | @property (strong, nonatomic) IBOutlet CLTokenInputView *secondTokenInputView; 18 | @property (strong, nonatomic) IBOutlet UITableView *tableView; 19 | @property (strong, nonatomic) IBOutlet NSLayoutConstraint *tableViewTopLayoutConstraint; 20 | @end 21 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // CLTokenInputViewController.m 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import "CLTokenInputViewController.h" 10 | 11 | #import "CLToken.h" 12 | 13 | @interface CLTokenInputViewController () 14 | 15 | @property (strong, nonatomic) NSArray *names; 16 | @property (strong, nonatomic) NSArray *filteredNames; 17 | 18 | @property (strong, nonatomic) NSMutableArray *selectedNames; 19 | 20 | @end 21 | 22 | @implementation CLTokenInputViewController 23 | 24 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 25 | { 26 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 27 | if (self) { 28 | // Custom initialization 29 | self.navigationItem.title = @"Token Input Test"; 30 | self.names = @[@"Brenden Mulligan", 31 | @"Cluster Labs, Inc.", 32 | @"Pat Fives", 33 | @"Rizwan Sattar", 34 | @"Taylor Hughes"]; 35 | self.filteredNames = nil; 36 | self.selectedNames = [NSMutableArray arrayWithCapacity:self.names.count]; 37 | 38 | } 39 | return self; 40 | } 41 | 42 | - (void)viewDidLoad 43 | { 44 | [super viewDidLoad]; 45 | // Do any additional setup after loading the view from its nib. 46 | if (![self respondsToSelector:@selector(automaticallyAdjustsScrollViewInsets)]) { 47 | self.tokenInputTopSpace.constant = 0.0; 48 | } 49 | UIButton *infoButton = [UIButton buttonWithType:UIButtonTypeInfoDark]; 50 | [infoButton addTarget:self action:@selector(onFieldInfoButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; 51 | self.tokenInputView.fieldName = @"To:"; 52 | self.tokenInputView.fieldView = infoButton; 53 | self.tokenInputView.placeholderText = @"Enter a name"; 54 | self.tokenInputView.accessoryView = [self contactAddButton]; 55 | self.tokenInputView.drawBottomBorder = YES; 56 | 57 | self.secondTokenInputView.fieldName = NSLocalizedString(@"Cc:", nil); 58 | self.secondTokenInputView.drawBottomBorder = YES; 59 | self.secondTokenInputView.delegate = self; 60 | [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"]; 61 | } 62 | 63 | - (void)didReceiveMemoryWarning 64 | { 65 | [super didReceiveMemoryWarning]; 66 | // Dispose of any resources that can be recreated. 67 | } 68 | 69 | - (void)viewDidAppear:(BOOL)animated 70 | { 71 | if (!self.tokenInputView.editing) { 72 | [self.tokenInputView beginEditing]; 73 | } 74 | [super viewDidAppear:animated]; 75 | } 76 | 77 | 78 | #pragma mark - CLTokenInputViewDelegate 79 | 80 | - (void)tokenInputView:(CLTokenInputView *)view didChangeText:(NSString *)text 81 | { 82 | if ([text isEqualToString:@""]){ 83 | self.filteredNames = nil; 84 | self.tableView.hidden = YES; 85 | } else { 86 | NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self contains[cd] %@", text]; 87 | self.filteredNames = [self.names filteredArrayUsingPredicate:predicate]; 88 | self.tableView.hidden = NO; 89 | } 90 | [self.tableView reloadData]; 91 | } 92 | 93 | - (void)tokenInputView:(CLTokenInputView *)view didAddToken:(CLToken *)token 94 | { 95 | NSString *name = token.displayText; 96 | [self.selectedNames addObject:name]; 97 | } 98 | 99 | - (void)tokenInputView:(CLTokenInputView *)view didRemoveToken:(CLToken *)token 100 | { 101 | NSString *name = token.displayText; 102 | [self.selectedNames removeObject:name]; 103 | } 104 | 105 | - (CLToken *)tokenInputView:(CLTokenInputView *)view tokenForText:(NSString *)text 106 | { 107 | if (self.filteredNames.count > 0) { 108 | NSString *matchingName = self.filteredNames[0]; 109 | CLToken *match = [[CLToken alloc] initWithDisplayText:matchingName context:nil]; 110 | return match; 111 | } 112 | // TODO: Perhaps if the text is a valid phone number, or email address, create a token 113 | // to "accept" it. 114 | return nil; 115 | } 116 | 117 | - (void)tokenInputViewDidEndEditing:(CLTokenInputView *)view 118 | { 119 | NSLog(@"token input view did end editing: %@", view); 120 | view.accessoryView = nil; 121 | } 122 | 123 | - (void)tokenInputViewDidBeginEditing:(CLTokenInputView *)view 124 | { 125 | 126 | NSLog(@"token input view did begin editing: %@", view); 127 | view.accessoryView = [self contactAddButton]; 128 | [self.view removeConstraint:self.tableViewTopLayoutConstraint]; 129 | self.tableViewTopLayoutConstraint = [NSLayoutConstraint constraintWithItem:self.tableView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0]; 130 | [self.view addConstraint:self.tableViewTopLayoutConstraint]; 131 | [self.view layoutIfNeeded]; 132 | } 133 | 134 | 135 | #pragma mark - UITableViewDataSource 136 | 137 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 138 | { 139 | return self.filteredNames.count; 140 | } 141 | 142 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 143 | { 144 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; 145 | NSString *name = self.filteredNames[indexPath.row]; 146 | cell.textLabel.text = name; 147 | if ([self.selectedNames containsObject:name]) { 148 | cell.accessoryType = UITableViewCellAccessoryCheckmark; 149 | } else { 150 | cell.accessoryType = UITableViewCellAccessoryNone; 151 | } 152 | return cell; 153 | } 154 | 155 | 156 | #pragma mark - UITableViewDelegate 157 | 158 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 159 | { 160 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 161 | 162 | NSString *name = self.filteredNames[indexPath.row]; 163 | CLToken *token = [[CLToken alloc] initWithDisplayText:name context:nil]; 164 | if (self.tokenInputView.isEditing) { 165 | [self.tokenInputView addToken:token]; 166 | } 167 | else if(self.secondTokenInputView.isEditing){ 168 | [self.secondTokenInputView addToken:token]; 169 | } 170 | } 171 | 172 | 173 | #pragma mark - Demo Button Actions 174 | 175 | 176 | - (void)onFieldInfoButtonTapped:(id)sender 177 | { 178 | UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Field View Button" 179 | message:@"This view is optional and can be a UIButton, etc." 180 | delegate:nil 181 | cancelButtonTitle:@"Okay" 182 | otherButtonTitles:nil]; 183 | [alertView show]; 184 | } 185 | 186 | 187 | - (void)onAccessoryContactAddButtonTapped:(id)sender 188 | { 189 | UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Accessory View Button" 190 | message:@"This view is optional and can be a UIButton, etc." 191 | delegate:nil 192 | cancelButtonTitle:@"Okay" 193 | otherButtonTitles:nil]; 194 | [alertView show]; 195 | } 196 | 197 | #pragma mark - Demo Buttons 198 | - (UIButton *)contactAddButton 199 | { 200 | UIButton *contactAddButton = [UIButton buttonWithType:UIButtonTypeContactAdd]; 201 | [contactAddButton addTarget:self action:@selector(onAccessoryContactAddButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; 202 | return contactAddButton; 203 | } 204 | 205 | @end 206 | -------------------------------------------------------------------------------- /CLTokenInputView/CLTokenInputViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /CLTokenInputView/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /CLTokenInputView/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /CLTokenInputView/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /CLTokenInputView/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /CLTokenInputView/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // CLTokenInputView 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "CLAppDelegate.h" 12 | 13 | int main(int argc, char * argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([CLAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CLTokenInputViewTests/CLTokenInputViewTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /CLTokenInputViewTests/CLTokenInputViewTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // CLTokenInputViewTests.m 3 | // CLTokenInputViewTests 4 | // 5 | // Created by Rizwan Sattar on 2/24/14. 6 | // Copyright (c) 2014 Cluster Labs, Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CLTokenInputViewTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation CLTokenInputViewTests 16 | 17 | - (void)setUp 18 | { 19 | [super setUp]; 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | - (void)tearDown 24 | { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | [super tearDown]; 27 | } 28 | 29 | - (void)testExample 30 | { 31 | XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /CLTokenInputViewTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Cluster Labs, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CLTokenInputView 2 | 3 | ![Image](http://cl.ly/image/1y3Q0u0q1N3H/iOS%20Simulator%20Screen%20Shot%20Jan%2028,%202015,%204.30.15%20PM.png) 4 | [![Screencap GFY](http://zippy.gfycat.com/ImpressiveRapidGelding.gif)](http://gfycat.com/ImpressiveRapidGelding) 5 | 6 | 7 | ## About 8 | 9 | `CLTokenInputView` is an almost pixel perfect replica of the input portion iOS's native contacts picker, used in Mail.app and Messages.app when composing a new message. 10 | 11 | Check out the sample view controller which uses CLTokenInputView to see how to incorporate it into your UI. We use this in our apps at [Cluster Labs, Inc.](https://cluster.co). 12 | 13 | Check out [a Swift port of this library](https://github.com/rlaferla/CLTokenInputView-Swift) by [@rlaferla](https://github.com/rlaferla). 14 | 15 | ### Note 16 | It ***does not*** provide the autocomplete drop down and matching; you must provide that yourself, so that `CLTokenInputView` can remain very generic and flexible. You can copy what the sample app is doing to show an autocompleting table view and maintain a list of the selected "tokens". 17 | 18 | ## Usage 19 | 20 | To run the example project, clone the repo, and open the Xcode project. You should use this on iOS 7 and up. 21 | 22 | To use this in your code, you should add an instance of `CLTokenInputView` to your view hierarchy. Typically it should be anchored to the top of your UI and to the sides. Using Autolayout `CLTokenInputView` can grow by itself, but if you need to control it manually, you can use the delegate. 23 | 24 | You should implement: 25 | 26 | ```objc 27 | - (void)tokenInputView:(CLTokenInputView *)view didChangeText:(NSString *)text 28 | { 29 | // Update your autocompletion table results with the text 30 | } 31 | ``` 32 | 33 | When the user taps on one of your autocomplete items, you should call: `-addToken:` on token input view. Example: 34 | 35 | ```objc 36 | NSString *name = self.filteredNames[indexPath.row]; 37 | CLToken *token = [[CLToken alloc] initWithDisplayText:name context:nil]; 38 | [self.tokenInputView addToken:token]; 39 | ``` 40 | 41 | Be sure to listen for: 42 | 43 | ```objc 44 | - (void)tokenInputView:(CLTokenInputView *)view didAddToken:(CLToken *)token; 45 | - (void)tokenInputView:(CLTokenInputView *)view didRemoveToken:(CLToken *)token; 46 | ``` 47 | ...and update your local data model of selected items. 48 | 49 | Lastly, you can implement: 50 | 51 | ```objc 52 | - (CLToken *)tokenInputView:(CLTokenInputView *)view tokenForText:(NSString *)text 53 | { 54 | // Return a CLToken instance that matches the text that has been entered. 55 | // Return nil if nothing matches 56 | } 57 | ``` 58 | ... so that a user can typically select the first result in your autocomplete. 59 | 60 | ## Customization 61 | 62 | `CLTokenInputView` is customizable using: 63 | 64 | - `tintColor` — Adjust the selection and text colors. 65 | - `fieldView` — (Optional) View to show to the top left of the tokens. 66 | - `fieldName` — (Optional, but recommended) Text to show before the token list (e.g. **"To:"**) 67 | - `placeholderText` — (Optional, but recommended) Text to show as a hint for the text field. 68 | - `accessoryView` — (Optional) View to show on the top right. (Often to launch a contact picker, like in Mail.app). 69 | - `keyboardType` — Adjust the keyboard type (`UIKeyboardType`). 70 | - `autocapitalizationType` — Adjust the capitalization behavior (`UITextAutocapitalizationType`). 71 | - `autocorrectionType` — Adjust the autocorrection behavior (`UITextAutocorrectionType`). 72 | - `drawBottomBorder` — Set to YES if CLTokenInputView should draw a native-style border below itself. 73 | 74 | ## Things I'd Like To Do: 75 | 76 | - Build the "collapsed" mode like in Mail.app which replaces the token UI with "[first-item] and N more" 77 | - Call search about 150ms after pausing typing 78 | - Scroll text field into position after typing 79 | - (Maybe?) Look into adding a very generic, flexible autocomplete UI? 80 | 81 | ## Installation 82 | 83 | CLTokenInputView is available through [CocoaPods](http://cocoapods.org). To install 84 | it, simply add the following line to your Podfile: 85 | 86 | pod "CLTokenInputView" 87 | 88 | Or, you can take all the .h and .m files from `CLTokenInputView/CLTokenInputView`. 89 | 90 | ## Author 91 | 92 | Cluster Labs, Inc., info@getcluster.com 93 | 94 | ## License 95 | 96 | CLTokenInputView is available under the MIT license. See the LICENSE file for more info. 97 | 98 | 99 | --------------------------------------------------------------------------------