├── GitHubImages ├── hashVideo.gif └── UITextViewStoryboard.png ├── textViewSample.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── robertchen.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── textViewSample.xccheckout ├── xcuserdata │ └── robertchen.xcuserdatad │ │ └── xcschemes │ │ ├── xcschememanagement.plist │ │ └── textViewSample.xcscheme └── project.pbxproj ├── textViewSample ├── TextCell.swift ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── UITextField+Extension.swift └── ViewController.swift ├── textViewSampleTests ├── Info.plist └── textViewSampleTests.swift └── README.md /GitHubImages/hashVideo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/SwiftTextViewHashtag/HEAD/GitHubImages/hashVideo.gif -------------------------------------------------------------------------------- /GitHubImages/UITextViewStoryboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/SwiftTextViewHashtag/HEAD/GitHubImages/UITextViewStoryboard.png -------------------------------------------------------------------------------- /textViewSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /textViewSample.xcodeproj/project.xcworkspace/xcuserdata/robertchen.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThornTechPublic/SwiftTextViewHashtag/HEAD/textViewSample.xcodeproj/project.xcworkspace/xcuserdata/robertchen.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /textViewSample/TextCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextCell.swift 3 | // textViewSample 4 | // 5 | // Created by Robert Chen on 5/8/15. 6 | // Copyright (c) 2015 Thorn Technologies. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TextCell : UITableViewCell { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | func setCellText(text:String){ 16 | textView.text = text 17 | textView.resolveHashTags() 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /textViewSample.xcodeproj/xcuserdata/robertchen.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | textViewSample.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | FC6AF2831AFD59A7003BAE37 16 | 17 | primary 18 | 19 | 20 | FC6AF2981AFD59A7003BAE37 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /textViewSampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /textViewSampleTests/textViewSampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // textViewSampleTests.swift 3 | // textViewSampleTests 4 | // 5 | // Created by Robert Chen on 5/8/15. 6 | // Copyright (c) 2015 Thorn Technologies. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class textViewSampleTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /textViewSample/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 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /textViewSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /textViewSample.xcodeproj/project.xcworkspace/xcshareddata/textViewSample.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | 2290921F-A0C7-4874-98C2-95AF7C777D30 9 | IDESourceControlProjectName 10 | textViewSample 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 2F057DB66CD94F58C2DFE665E1E7F44057186C1C 14 | https://github.com/ThornTechPublic/SwiftTextViewHashtag.git 15 | 16 | IDESourceControlProjectPath 17 | textViewSample.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | 2F057DB66CD94F58C2DFE665E1E7F44057186C1C 21 | ../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/ThornTechPublic/SwiftTextViewHashtag.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | 2F057DB66CD94F58C2DFE665E1E7F44057186C1C 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | 2F057DB66CD94F58C2DFE665E1E7F44057186C1C 36 | IDESourceControlWCCName 37 | SwiftTextViewHashtag 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /textViewSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // textViewSample 4 | // 5 | // Created by Robert Chen on 5/8/15. 6 | // Copyright (c) 2015 Thorn Technologies. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /textViewSample/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftTextViewHashtag 2 | Hash tag detection for TextViews in Swift 3 | 4 | ## Overview 5 | 6 | This is a quick and dirty sample project implementing Hashtag detection 7 | 8 | ![animated gif demo](https://github.com/ThornTechPublic/SwiftTextViewHashtag/blob/master/GitHubImages/hashVideo.gif) 9 | 10 | ## Installation / Integration 11 | 12 | To use this in your own app, just copy the [UITextField+Extension.swift](https://github.com/ThornTechPublic/SwiftTextViewHashtag/blob/master/textViewSample/UITextField%2BExtension.swift) file into your project. 13 | 14 | You may want to further [customize this section of code](https://github.com/ThornTechPublic/SwiftTextViewHashtag/blob/master/textViewSample/UITextField%2BExtension.swift#L27) with a TextView font and color that fits your app's style guide, because the Storyboard attributes get overridden. 15 | 16 | ## Usage 17 | 18 | To detect hashtags in your textViews: 19 | 20 | 1. Make sure the UITextView is `selectable`, can detect `links`, and is not `editable` (see image below) 21 | 1. Wire the UITextView delegate to your ViewController 22 | 1. Implement the UITextViewDelegate method `textView:shouldInteractWithURL:` to hook into the URL tap. [See example](https://github.com/ThornTechPublic/SwiftTextViewHashtag/blob/master/textViewSample/ViewController.swift#L164). 23 | 1. After you set the text, call the `resolveHashTags()` method. [See example](https://github.com/ThornTechPublic/SwiftTextViewHashtag/blob/master/textViewSample/TextCell.swift#L17) 24 | 25 | ![screenshot from storyboard](https://github.com/ThornTechPublic/SwiftTextViewHashtag/blob/master/GitHubImages/UITextViewStoryboard.png) 26 | 27 | ## How it works 28 | 29 | The approach used here is to add an attribute (much like an "href") to hashtagged words. 30 | 31 | ### URL detection 32 | 33 | Hashtag detection builds upon URL detection. This is why the TextView is setup to detect links. Note that links are only clickable when `Editable` is unchecked. 34 | 35 | At this point, URLs in the textview will open in the Safari app. 36 | 37 | ### Add link attributes 38 | 39 | Next, an "href" attribute is added to each hashtagged word. 40 | 41 | The overall process goes something like this: 42 | * Iterate over each word and look for anything that starts with `#` 43 | * Chop off the first character `#`. For example, `#helloWorld` becomes `helloWorld` 44 | * Create a fake URL using a fake URL scheme. For example, `hash:helloWorld` 45 | * Associate this fake URL with the hashtag word. `NSMutableAttributedString` has APIs to accomplish this. 46 | 47 | [Here's the code](https://github.com/ribl/SwiftTextViewHashtag/blob/master/textViewSample/UITextField%2BExtension.swift#L13) 48 | 49 | ### Clicking on hashtags 50 | 51 | Now that hashtags are URLs, they are a different color and can be clicked. Note: it's tempting to add a tap gesture to TextView, but you can leverage the built-in delegate instead. 52 | 53 | Intercept the URL click and check for your fake URL scheme. 54 | * Set the TextView delegate. 55 | * Implement the `UITextFieldDelegate` which has a `shouldInteractWithURL` method 56 | * Check for your fake `URL.scheme` 57 | * Grab the payload in the `URL.resourceSpecifier` 58 | 59 | [Here's the code](https://github.com/ThornTechPublic/SwiftTextViewHashtag/blob/master/textViewSample/ViewController.swift#L164) 60 | 61 | ## Other resources 62 | 63 | * [STTweetLabel](https://github.com/SebastienThiebaud/STTweetLabel), an Objective-C CocoaPod for hashtag detection 64 | * A [swift implementation](https://yeti.co/blog/hashtags-and-mentions/) of hashtags and mentions. I wish this was available when I first implemented hashtags. Their approach is slightly different though. 65 | * I used [this](http://kishikawakatsumi.hatenablog.com/entry/20130605/1370370925) to initially figure out my approach. You might need to click "translate from japanese" on the top. 66 | * I used [this](http://stackoverflow.com/questions/11547919/check-if-string-contains-a-hashtag-and-then-change-hashtag-color) to figure out how to use `NSMutableAttributedString` 67 | * [Ray Wenderlich Scroll View](http://www.raywenderlich.com/video-tutorials#swiftscrollview) video series helped me understand keyboard movement in the example project. 68 | -------------------------------------------------------------------------------- /textViewSample/UITextField+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITextField+Extension.swift 3 | // textViewSample 4 | // 5 | // Created by Robert Chen on 5/22/15. 6 | // Copyright (c) 2015 Thorn Technologies. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | func += ( left: inout Dictionary, right: Dictionary) { 12 | for (k, v) in right { 13 | left.updateValue(v, forKey: k) 14 | } 15 | } 16 | 17 | extension String { 18 | func NSRangeFromRange(range: Range) -> NSRange { 19 | let utf16view = self.utf16 20 | let from = String.UTF16View.Index(range.lowerBound, within: utf16view) 21 | let to = String.UTF16View.Index(range.upperBound, within: utf16view) 22 | return NSMakeRange(utf16view.distance(from: utf16view.startIndex, to: from), 23 | utf16view.distance(from: from, to: to)) 24 | } 25 | 26 | mutating func dropTrailingNonAlphaNumericCharacters() { 27 | let nonAlphaNumericCharacters = NSCharacterSet.alphanumerics.inverted 28 | let characterArray = components(separatedBy: nonAlphaNumericCharacters) 29 | if let first = characterArray.first { 30 | self = first 31 | } 32 | } 33 | } 34 | 35 | extension UITextView { 36 | 37 | public func resolveHashTags(possibleUserDisplayNames:[String]? = nil) { 38 | 39 | let schemeMap = [ 40 | "#":"hash", 41 | "@":"mention" 42 | ] 43 | 44 | // Separate the string into individual words. 45 | // Whitespace is used as the word boundary. 46 | // You might see word boundaries at special characters, like before a period. 47 | // But we need to be careful to retain the # or @ characters. 48 | let words = self.text.components(separatedBy: NSCharacterSet.whitespacesAndNewlines) 49 | let attributedString = attributedText.mutableCopy() as! NSMutableAttributedString 50 | 51 | // keep track of where we are as we interate through the string. 52 | // otherwise, a string like "#test #test" will only highlight the first one. 53 | var bookmark = text.startIndex 54 | 55 | // Iterate over each word. 56 | // So far each word will look like: 57 | // - I 58 | // - visited 59 | // - #123abc.go! 60 | // The last word is a hashtag of #123abc 61 | // Use the following hashtag rules: 62 | // - Include the hashtag # in the URL 63 | // - Only include alphanumeric characters. Special chars and anything after are chopped off. 64 | // - Hashtags can start with numbers. But the whole thing can't be a number (#123abc is ok, #123 is not) 65 | for word in words { 66 | 67 | var scheme:String? = nil 68 | 69 | if word.hasPrefix("#") { 70 | scheme = schemeMap["#"] 71 | } else if word.hasPrefix("@") { 72 | scheme = schemeMap["@"] 73 | } 74 | 75 | // Drop the # or @ 76 | var wordWithTagRemoved = String(word.characters.dropFirst()) 77 | 78 | // Drop any trailing punctuation 79 | wordWithTagRemoved.dropTrailingNonAlphaNumericCharacters() 80 | 81 | // Make sure we still have a valid word (i.e. not just '#' or '@' by itself, not #100) 82 | guard let schemeMatch = scheme, Int(wordWithTagRemoved) == nil && !wordWithTagRemoved.isEmpty 83 | else { continue } 84 | 85 | let remainingRange = Range(bookmark.. 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 75 | 77 | 83 | 84 | 85 | 86 | 87 | 88 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /textViewSample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // textViewSample 4 | // 5 | // Created by Robert Chen on 5/8/15. 6 | // Copyright (c) 2015 Thorn Technologies. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | var lastTextViewHeight:CGFloat = 0.0 14 | 15 | var messages:[String] = ["regular text, nothing to see here", "#ribl <- click on it to see an alert", "@riblapp mention tags work too", "regular urls are clickable http://ribl.co", "add your own text below"] 16 | 17 | @IBOutlet weak var tableView: UITableView! 18 | @IBOutlet weak var toolbarBottom: NSLayoutConstraint! 19 | @IBOutlet weak var textView: UITextView! 20 | @IBOutlet weak var placeholderText: UILabel! 21 | @IBOutlet weak var textviewHeight: NSLayoutConstraint! 22 | 23 | @IBAction func sendButton(sender: AnyObject) { 24 | textView.endEditing(true) 25 | messages.append(textView.text) 26 | // clear the text 27 | textView.text = "" 28 | // and manually trigger the delegate method 29 | self.textViewDidChange(textView) 30 | tableView.reloadData() 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | // listen for the keyboard going up and down 36 | keyboardHeightRegisterNotifications() 37 | } 38 | 39 | } 40 | 41 | // MARK: - Keyboard helper methods 42 | 43 | // all this just to move the keyboard up and down. 44 | extension ViewController { 45 | 46 | /// register keyboard notifications to shift the scrollview content insets 47 | func keyboardHeightRegisterNotifications() { 48 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.keyboardWillShow(_:)), name: UIKeyboardWillShowNotification, object: nil) 49 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.keyboardWillHide(_:)), name: UIKeyboardWillHideNotification, object: nil) 50 | } 51 | 52 | /// just pass true or false if you're shifting the keyboard up or down 53 | func keyboardWillShow(notification: NSNotification) { 54 | adjustInsetForKeyboardShow(true, notification: notification) 55 | } 56 | 57 | func keyboardWillHide(notification: NSNotification) { 58 | adjustInsetForKeyboardShow(false, notification: notification) 59 | } 60 | 61 | /// consolidate the keyboard movement logic into one method, and just pass a boolean for up or down. 62 | func adjustInsetForKeyboardShow(show: Bool, notification: NSNotification) { 63 | // some implementations out there use -1 and 1 to move it up or down. 64 | // debugging is a little easier if you use -1 and 0 instead. 65 | toolbarBottom.constant = getKeyboardHeight(notification) * (show ? 1 : 0) 66 | // normally, the constraint change is updated immediately. 67 | // by simply added UIView.animateWithDuration along with a layoutIfNeeded(), 68 | // the constraint change will happen in the animation. 69 | // the animation settings below sort of match the keyboard animation 70 | UIView.animateWithDuration(0.5, 71 | delay: 0.0, 72 | options: .CurveEaseInOut, 73 | animations: { 74 | // animate the constraint change 75 | self.view.layoutIfNeeded() 76 | }, 77 | completion: nil 78 | ) 79 | } 80 | 81 | func getKeyboardHeight(notification: NSNotification) -> CGFloat{ 82 | // == userInfo || {} 83 | let userInfo = notification.userInfo ?? [:] 84 | // CGRect wrapped in a NSValue 85 | // Make sure you use UIKeyboardFrameEndUserInfoKey, NOT UIKeyboardFrameBeginUserInfoKey 86 | // "End" is good. "Begin" is bad. 87 | // To test, switch keyboards and make sure the heights are correct. 88 | let keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue() 89 | return CGRectGetHeight(keyboardFrame) 90 | } 91 | 92 | } 93 | 94 | // MARK: - UITableViewDataSource methods 95 | 96 | extension ViewController : UITableViewDataSource { 97 | 98 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 99 | return messages.count 100 | } 101 | 102 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 103 | let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! TextCell 104 | cell.setCellText(messages[indexPath.row]) 105 | return cell 106 | } 107 | 108 | } 109 | 110 | // MARK: - UITableViewDelegate methods 111 | 112 | extension ViewController : UITableViewDelegate { 113 | 114 | func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { 115 | // set the height of the row based on the text height. 116 | // TODO: there's probably a better way to do this. 117 | 118 | // type cast to NSString to get additional methods 119 | let myString: NSString = messages[indexPath.row] as NSString 120 | // label height depends on font 121 | let attributes = [NSFontAttributeName: UIFont.systemFontOfSize(18.0)] 122 | let stringWidth:CGFloat = UIScreen.mainScreen().bounds.width - 20 123 | // hard-coding a really big number that exceeds the screen height 124 | let infiniteHeight:CGFloat = 1600 125 | let temporarySize = CGSizeMake(stringWidth, infiniteHeight) 126 | let rect:CGRect = myString.boundingRectWithSize(temporarySize, options: .UsesLineFragmentOrigin, attributes: attributes, context: nil) 127 | return rect.height + 30 128 | } 129 | 130 | } 131 | 132 | // MARK: - UITextViewDelegate methods 133 | 134 | extension ViewController : UITextViewDelegate { 135 | 136 | // increase the height of the textview as the user types 137 | func textViewDidChange(textView: UITextView){ 138 | // hide placeholder text 139 | placeholderText.hidden = !textView.text.isEmpty 140 | // create a hypothetical tall box that contains the text. 141 | // then shrink down the height based on the content. 142 | let newSize:CGSize = textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: 1600.0)) 143 | // remember that new height 144 | let newHeight = newSize.height 145 | // change the height constraint only if it's different. 146 | // otherwise, it get set on every single character the user types. 147 | if lastTextViewHeight != newHeight { 148 | lastTextViewHeight = newHeight 149 | // the 7.0 is to account for the top of the text getting scrolled up slightly 150 | // to account for a potential new line 151 | textviewHeight.constant = newSize.height + 7.0 152 | } 153 | } 154 | 155 | func showHashTagAlert(tagType:String, payload:String){ 156 | let alertView = UIAlertView() 157 | alertView.title = "\(tagType) tag detected" 158 | // get a handle on the payload 159 | alertView.message = "\(payload)" 160 | alertView.addButtonWithTitle("Ok") 161 | alertView.show() 162 | } 163 | 164 | func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool { 165 | // check for our fake URL scheme hash:helloWorld 166 | switch URL.scheme { 167 | case "hash" : 168 | showHashTagAlert("hash", payload: URL.resourceSpecifier.stringByRemovingPercentEncoding!) 169 | case "mention" : 170 | showHashTagAlert("mention", payload: URL.resourceSpecifier.stringByRemovingPercentEncoding!) 171 | default: 172 | print("just a regular url") 173 | } 174 | 175 | return true 176 | } 177 | 178 | } -------------------------------------------------------------------------------- /textViewSample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 91 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /textViewSample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FC32ED3A1B0FB23100FFBFF6 /* UITextField+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC32ED391B0FB23100FFBFF6 /* UITextField+Extension.swift */; }; 11 | FC6AF28A1AFD59A7003BAE37 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC6AF2891AFD59A7003BAE37 /* AppDelegate.swift */; }; 12 | FC6AF28C1AFD59A7003BAE37 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC6AF28B1AFD59A7003BAE37 /* ViewController.swift */; }; 13 | FC6AF28F1AFD59A7003BAE37 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FC6AF28D1AFD59A7003BAE37 /* Main.storyboard */; }; 14 | FC6AF2911AFD59A7003BAE37 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FC6AF2901AFD59A7003BAE37 /* Images.xcassets */; }; 15 | FC6AF2941AFD59A7003BAE37 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = FC6AF2921AFD59A7003BAE37 /* LaunchScreen.xib */; }; 16 | FC6AF2A01AFD59A7003BAE37 /* textViewSampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC6AF29F1AFD59A7003BAE37 /* textViewSampleTests.swift */; }; 17 | FC6AF2AA1AFD66EE003BAE37 /* TextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC6AF2A91AFD66EE003BAE37 /* TextCell.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | FC6AF29A1AFD59A7003BAE37 /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = FC6AF27C1AFD59A7003BAE37 /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = FC6AF2831AFD59A7003BAE37; 26 | remoteInfo = textViewSample; 27 | }; 28 | /* End PBXContainerItemProxy section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | FC32ED391B0FB23100FFBFF6 /* UITextField+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextField+Extension.swift"; sourceTree = ""; }; 32 | FC6AF2841AFD59A7003BAE37 /* textViewSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = textViewSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | FC6AF2881AFD59A7003BAE37 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 34 | FC6AF2891AFD59A7003BAE37 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 35 | FC6AF28B1AFD59A7003BAE37 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 36 | FC6AF28E1AFD59A7003BAE37 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 37 | FC6AF2901AFD59A7003BAE37 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 38 | FC6AF2931AFD59A7003BAE37 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 39 | FC6AF2991AFD59A7003BAE37 /* textViewSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = textViewSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | FC6AF29E1AFD59A7003BAE37 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | FC6AF29F1AFD59A7003BAE37 /* textViewSampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = textViewSampleTests.swift; sourceTree = ""; }; 42 | FC6AF2A91AFD66EE003BAE37 /* TextCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextCell.swift; sourceTree = ""; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | FC6AF2811AFD59A7003BAE37 /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | FC6AF2961AFD59A7003BAE37 /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | /* End PBXFrameworksBuildPhase section */ 61 | 62 | /* Begin PBXGroup section */ 63 | FC6AF27B1AFD59A7003BAE37 = { 64 | isa = PBXGroup; 65 | children = ( 66 | FC6AF2861AFD59A7003BAE37 /* textViewSample */, 67 | FC6AF29C1AFD59A7003BAE37 /* textViewSampleTests */, 68 | FC6AF2851AFD59A7003BAE37 /* Products */, 69 | ); 70 | sourceTree = ""; 71 | }; 72 | FC6AF2851AFD59A7003BAE37 /* Products */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | FC6AF2841AFD59A7003BAE37 /* textViewSample.app */, 76 | FC6AF2991AFD59A7003BAE37 /* textViewSampleTests.xctest */, 77 | ); 78 | name = Products; 79 | sourceTree = ""; 80 | }; 81 | FC6AF2861AFD59A7003BAE37 /* textViewSample */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | FC6AF2891AFD59A7003BAE37 /* AppDelegate.swift */, 85 | FC6AF28B1AFD59A7003BAE37 /* ViewController.swift */, 86 | FC6AF2A91AFD66EE003BAE37 /* TextCell.swift */, 87 | FC32ED391B0FB23100FFBFF6 /* UITextField+Extension.swift */, 88 | FC6AF28D1AFD59A7003BAE37 /* Main.storyboard */, 89 | FC6AF2901AFD59A7003BAE37 /* Images.xcassets */, 90 | FC6AF2921AFD59A7003BAE37 /* LaunchScreen.xib */, 91 | FC6AF2871AFD59A7003BAE37 /* Supporting Files */, 92 | ); 93 | path = textViewSample; 94 | sourceTree = ""; 95 | }; 96 | FC6AF2871AFD59A7003BAE37 /* Supporting Files */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | FC6AF2881AFD59A7003BAE37 /* Info.plist */, 100 | ); 101 | name = "Supporting Files"; 102 | sourceTree = ""; 103 | }; 104 | FC6AF29C1AFD59A7003BAE37 /* textViewSampleTests */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | FC6AF29F1AFD59A7003BAE37 /* textViewSampleTests.swift */, 108 | FC6AF29D1AFD59A7003BAE37 /* Supporting Files */, 109 | ); 110 | path = textViewSampleTests; 111 | sourceTree = ""; 112 | }; 113 | FC6AF29D1AFD59A7003BAE37 /* Supporting Files */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | FC6AF29E1AFD59A7003BAE37 /* Info.plist */, 117 | ); 118 | name = "Supporting Files"; 119 | sourceTree = ""; 120 | }; 121 | /* End PBXGroup section */ 122 | 123 | /* Begin PBXNativeTarget section */ 124 | FC6AF2831AFD59A7003BAE37 /* textViewSample */ = { 125 | isa = PBXNativeTarget; 126 | buildConfigurationList = FC6AF2A31AFD59A7003BAE37 /* Build configuration list for PBXNativeTarget "textViewSample" */; 127 | buildPhases = ( 128 | FC6AF2801AFD59A7003BAE37 /* Sources */, 129 | FC6AF2811AFD59A7003BAE37 /* Frameworks */, 130 | FC6AF2821AFD59A7003BAE37 /* Resources */, 131 | ); 132 | buildRules = ( 133 | ); 134 | dependencies = ( 135 | ); 136 | name = textViewSample; 137 | productName = textViewSample; 138 | productReference = FC6AF2841AFD59A7003BAE37 /* textViewSample.app */; 139 | productType = "com.apple.product-type.application"; 140 | }; 141 | FC6AF2981AFD59A7003BAE37 /* textViewSampleTests */ = { 142 | isa = PBXNativeTarget; 143 | buildConfigurationList = FC6AF2A61AFD59A7003BAE37 /* Build configuration list for PBXNativeTarget "textViewSampleTests" */; 144 | buildPhases = ( 145 | FC6AF2951AFD59A7003BAE37 /* Sources */, 146 | FC6AF2961AFD59A7003BAE37 /* Frameworks */, 147 | FC6AF2971AFD59A7003BAE37 /* Resources */, 148 | ); 149 | buildRules = ( 150 | ); 151 | dependencies = ( 152 | FC6AF29B1AFD59A7003BAE37 /* PBXTargetDependency */, 153 | ); 154 | name = textViewSampleTests; 155 | productName = textViewSampleTests; 156 | productReference = FC6AF2991AFD59A7003BAE37 /* textViewSampleTests.xctest */; 157 | productType = "com.apple.product-type.bundle.unit-test"; 158 | }; 159 | /* End PBXNativeTarget section */ 160 | 161 | /* Begin PBXProject section */ 162 | FC6AF27C1AFD59A7003BAE37 /* Project object */ = { 163 | isa = PBXProject; 164 | attributes = { 165 | LastSwiftMigration = 0720; 166 | LastSwiftUpdateCheck = 0720; 167 | LastUpgradeCheck = 0730; 168 | ORGANIZATIONNAME = "Thorn Technologies"; 169 | TargetAttributes = { 170 | FC6AF2831AFD59A7003BAE37 = { 171 | CreatedOnToolsVersion = 6.3.1; 172 | }; 173 | FC6AF2981AFD59A7003BAE37 = { 174 | CreatedOnToolsVersion = 6.3.1; 175 | TestTargetID = FC6AF2831AFD59A7003BAE37; 176 | }; 177 | }; 178 | }; 179 | buildConfigurationList = FC6AF27F1AFD59A7003BAE37 /* Build configuration list for PBXProject "textViewSample" */; 180 | compatibilityVersion = "Xcode 3.2"; 181 | developmentRegion = English; 182 | hasScannedForEncodings = 0; 183 | knownRegions = ( 184 | en, 185 | Base, 186 | ); 187 | mainGroup = FC6AF27B1AFD59A7003BAE37; 188 | productRefGroup = FC6AF2851AFD59A7003BAE37 /* Products */; 189 | projectDirPath = ""; 190 | projectRoot = ""; 191 | targets = ( 192 | FC6AF2831AFD59A7003BAE37 /* textViewSample */, 193 | FC6AF2981AFD59A7003BAE37 /* textViewSampleTests */, 194 | ); 195 | }; 196 | /* End PBXProject section */ 197 | 198 | /* Begin PBXResourcesBuildPhase section */ 199 | FC6AF2821AFD59A7003BAE37 /* Resources */ = { 200 | isa = PBXResourcesBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | FC6AF28F1AFD59A7003BAE37 /* Main.storyboard in Resources */, 204 | FC6AF2941AFD59A7003BAE37 /* LaunchScreen.xib in Resources */, 205 | FC6AF2911AFD59A7003BAE37 /* Images.xcassets in Resources */, 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | }; 209 | FC6AF2971AFD59A7003BAE37 /* Resources */ = { 210 | isa = PBXResourcesBuildPhase; 211 | buildActionMask = 2147483647; 212 | files = ( 213 | ); 214 | runOnlyForDeploymentPostprocessing = 0; 215 | }; 216 | /* End PBXResourcesBuildPhase section */ 217 | 218 | /* Begin PBXSourcesBuildPhase section */ 219 | FC6AF2801AFD59A7003BAE37 /* Sources */ = { 220 | isa = PBXSourcesBuildPhase; 221 | buildActionMask = 2147483647; 222 | files = ( 223 | FC6AF28C1AFD59A7003BAE37 /* ViewController.swift in Sources */, 224 | FC32ED3A1B0FB23100FFBFF6 /* UITextField+Extension.swift in Sources */, 225 | FC6AF2AA1AFD66EE003BAE37 /* TextCell.swift in Sources */, 226 | FC6AF28A1AFD59A7003BAE37 /* AppDelegate.swift in Sources */, 227 | ); 228 | runOnlyForDeploymentPostprocessing = 0; 229 | }; 230 | FC6AF2951AFD59A7003BAE37 /* Sources */ = { 231 | isa = PBXSourcesBuildPhase; 232 | buildActionMask = 2147483647; 233 | files = ( 234 | FC6AF2A01AFD59A7003BAE37 /* textViewSampleTests.swift in Sources */, 235 | ); 236 | runOnlyForDeploymentPostprocessing = 0; 237 | }; 238 | /* End PBXSourcesBuildPhase section */ 239 | 240 | /* Begin PBXTargetDependency section */ 241 | FC6AF29B1AFD59A7003BAE37 /* PBXTargetDependency */ = { 242 | isa = PBXTargetDependency; 243 | target = FC6AF2831AFD59A7003BAE37 /* textViewSample */; 244 | targetProxy = FC6AF29A1AFD59A7003BAE37 /* PBXContainerItemProxy */; 245 | }; 246 | /* End PBXTargetDependency section */ 247 | 248 | /* Begin PBXVariantGroup section */ 249 | FC6AF28D1AFD59A7003BAE37 /* Main.storyboard */ = { 250 | isa = PBXVariantGroup; 251 | children = ( 252 | FC6AF28E1AFD59A7003BAE37 /* Base */, 253 | ); 254 | name = Main.storyboard; 255 | sourceTree = ""; 256 | }; 257 | FC6AF2921AFD59A7003BAE37 /* LaunchScreen.xib */ = { 258 | isa = PBXVariantGroup; 259 | children = ( 260 | FC6AF2931AFD59A7003BAE37 /* Base */, 261 | ); 262 | name = LaunchScreen.xib; 263 | sourceTree = ""; 264 | }; 265 | /* End PBXVariantGroup section */ 266 | 267 | /* Begin XCBuildConfiguration section */ 268 | FC6AF2A11AFD59A7003BAE37 /* Debug */ = { 269 | isa = XCBuildConfiguration; 270 | buildSettings = { 271 | ALWAYS_SEARCH_USER_PATHS = NO; 272 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 273 | CLANG_CXX_LIBRARY = "libc++"; 274 | CLANG_ENABLE_MODULES = YES; 275 | CLANG_ENABLE_OBJC_ARC = YES; 276 | CLANG_WARN_BOOL_CONVERSION = YES; 277 | CLANG_WARN_CONSTANT_CONVERSION = YES; 278 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 279 | CLANG_WARN_EMPTY_BODY = YES; 280 | CLANG_WARN_ENUM_CONVERSION = YES; 281 | CLANG_WARN_INT_CONVERSION = YES; 282 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 283 | CLANG_WARN_UNREACHABLE_CODE = YES; 284 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 285 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 286 | COPY_PHASE_STRIP = NO; 287 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 288 | ENABLE_STRICT_OBJC_MSGSEND = YES; 289 | ENABLE_TESTABILITY = YES; 290 | GCC_C_LANGUAGE_STANDARD = gnu99; 291 | GCC_DYNAMIC_NO_PIC = NO; 292 | GCC_NO_COMMON_BLOCKS = YES; 293 | GCC_OPTIMIZATION_LEVEL = 0; 294 | GCC_PREPROCESSOR_DEFINITIONS = ( 295 | "DEBUG=1", 296 | "$(inherited)", 297 | ); 298 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 299 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 300 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 301 | GCC_WARN_UNDECLARED_SELECTOR = YES; 302 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 303 | GCC_WARN_UNUSED_FUNCTION = YES; 304 | GCC_WARN_UNUSED_VARIABLE = YES; 305 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 306 | MTL_ENABLE_DEBUG_INFO = YES; 307 | ONLY_ACTIVE_ARCH = YES; 308 | SDKROOT = iphoneos; 309 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 310 | TARGETED_DEVICE_FAMILY = "1,2"; 311 | }; 312 | name = Debug; 313 | }; 314 | FC6AF2A21AFD59A7003BAE37 /* Release */ = { 315 | isa = XCBuildConfiguration; 316 | buildSettings = { 317 | ALWAYS_SEARCH_USER_PATHS = NO; 318 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 319 | CLANG_CXX_LIBRARY = "libc++"; 320 | CLANG_ENABLE_MODULES = YES; 321 | CLANG_ENABLE_OBJC_ARC = YES; 322 | CLANG_WARN_BOOL_CONVERSION = YES; 323 | CLANG_WARN_CONSTANT_CONVERSION = YES; 324 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 325 | CLANG_WARN_EMPTY_BODY = YES; 326 | CLANG_WARN_ENUM_CONVERSION = YES; 327 | CLANG_WARN_INT_CONVERSION = YES; 328 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 329 | CLANG_WARN_UNREACHABLE_CODE = YES; 330 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 331 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 332 | COPY_PHASE_STRIP = NO; 333 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 334 | ENABLE_NS_ASSERTIONS = NO; 335 | ENABLE_STRICT_OBJC_MSGSEND = YES; 336 | GCC_C_LANGUAGE_STANDARD = gnu99; 337 | GCC_NO_COMMON_BLOCKS = YES; 338 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 339 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 340 | GCC_WARN_UNDECLARED_SELECTOR = YES; 341 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 342 | GCC_WARN_UNUSED_FUNCTION = YES; 343 | GCC_WARN_UNUSED_VARIABLE = YES; 344 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 345 | MTL_ENABLE_DEBUG_INFO = NO; 346 | SDKROOT = iphoneos; 347 | TARGETED_DEVICE_FAMILY = "1,2"; 348 | VALIDATE_PRODUCT = YES; 349 | }; 350 | name = Release; 351 | }; 352 | FC6AF2A41AFD59A7003BAE37 /* Debug */ = { 353 | isa = XCBuildConfiguration; 354 | buildSettings = { 355 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 356 | INFOPLIST_FILE = textViewSample/Info.plist; 357 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 358 | PRODUCT_BUNDLE_IDENTIFIER = "com.thorntech.$(PRODUCT_NAME:rfc1034identifier)"; 359 | PRODUCT_NAME = "$(TARGET_NAME)"; 360 | }; 361 | name = Debug; 362 | }; 363 | FC6AF2A51AFD59A7003BAE37 /* Release */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 367 | INFOPLIST_FILE = textViewSample/Info.plist; 368 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 369 | PRODUCT_BUNDLE_IDENTIFIER = "com.thorntech.$(PRODUCT_NAME:rfc1034identifier)"; 370 | PRODUCT_NAME = "$(TARGET_NAME)"; 371 | }; 372 | name = Release; 373 | }; 374 | FC6AF2A71AFD59A7003BAE37 /* Debug */ = { 375 | isa = XCBuildConfiguration; 376 | buildSettings = { 377 | BUNDLE_LOADER = "$(TEST_HOST)"; 378 | FRAMEWORK_SEARCH_PATHS = ( 379 | "$(SDKROOT)/Developer/Library/Frameworks", 380 | "$(inherited)", 381 | ); 382 | GCC_PREPROCESSOR_DEFINITIONS = ( 383 | "DEBUG=1", 384 | "$(inherited)", 385 | ); 386 | INFOPLIST_FILE = textViewSampleTests/Info.plist; 387 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 388 | PRODUCT_BUNDLE_IDENTIFIER = "com.thorntech.$(PRODUCT_NAME:rfc1034identifier)"; 389 | PRODUCT_NAME = "$(TARGET_NAME)"; 390 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/textViewSample.app/textViewSample"; 391 | }; 392 | name = Debug; 393 | }; 394 | FC6AF2A81AFD59A7003BAE37 /* Release */ = { 395 | isa = XCBuildConfiguration; 396 | buildSettings = { 397 | BUNDLE_LOADER = "$(TEST_HOST)"; 398 | FRAMEWORK_SEARCH_PATHS = ( 399 | "$(SDKROOT)/Developer/Library/Frameworks", 400 | "$(inherited)", 401 | ); 402 | INFOPLIST_FILE = textViewSampleTests/Info.plist; 403 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 404 | PRODUCT_BUNDLE_IDENTIFIER = "com.thorntech.$(PRODUCT_NAME:rfc1034identifier)"; 405 | PRODUCT_NAME = "$(TARGET_NAME)"; 406 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/textViewSample.app/textViewSample"; 407 | }; 408 | name = Release; 409 | }; 410 | /* End XCBuildConfiguration section */ 411 | 412 | /* Begin XCConfigurationList section */ 413 | FC6AF27F1AFD59A7003BAE37 /* Build configuration list for PBXProject "textViewSample" */ = { 414 | isa = XCConfigurationList; 415 | buildConfigurations = ( 416 | FC6AF2A11AFD59A7003BAE37 /* Debug */, 417 | FC6AF2A21AFD59A7003BAE37 /* Release */, 418 | ); 419 | defaultConfigurationIsVisible = 0; 420 | defaultConfigurationName = Release; 421 | }; 422 | FC6AF2A31AFD59A7003BAE37 /* Build configuration list for PBXNativeTarget "textViewSample" */ = { 423 | isa = XCConfigurationList; 424 | buildConfigurations = ( 425 | FC6AF2A41AFD59A7003BAE37 /* Debug */, 426 | FC6AF2A51AFD59A7003BAE37 /* Release */, 427 | ); 428 | defaultConfigurationIsVisible = 0; 429 | defaultConfigurationName = Release; 430 | }; 431 | FC6AF2A61AFD59A7003BAE37 /* Build configuration list for PBXNativeTarget "textViewSampleTests" */ = { 432 | isa = XCConfigurationList; 433 | buildConfigurations = ( 434 | FC6AF2A71AFD59A7003BAE37 /* Debug */, 435 | FC6AF2A81AFD59A7003BAE37 /* Release */, 436 | ); 437 | defaultConfigurationIsVisible = 0; 438 | defaultConfigurationName = Release; 439 | }; 440 | /* End XCConfigurationList section */ 441 | }; 442 | rootObject = FC6AF27C1AFD59A7003BAE37 /* Project object */; 443 | } 444 | --------------------------------------------------------------------------------