Repository: slackhq/SlackTextViewController Branch: master Commit: 178750fe96fe Files: 117 Total size: 550.9 KB Directory structure: gitextract_cu9877x5/ ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── .xctool-args ├── CHANGELOG.md ├── Examples/ │ ├── Messenger-Programatic/ │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Info.plist │ │ └── main.m │ ├── Messenger-Shared/ │ │ ├── Bridge-Header.h │ │ ├── Images.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Icons/ │ │ │ │ ├── Contents.json │ │ │ │ ├── icn_append.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── icn_arrow_down.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── icn_arrow_up.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── icn_editing.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── icn_pic.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── icn_typing.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── icn_upload.imageset/ │ │ │ │ └── Contents.json │ │ │ └── LaunchImage.launchimage/ │ │ │ └── Contents.json │ │ ├── Message.h │ │ ├── Message.m │ │ ├── MessageTableViewCell.h │ │ ├── MessageTableViewCell.m │ │ ├── MessageTextView.h │ │ ├── MessageTextView.m │ │ ├── MessageViewController.h │ │ ├── MessageViewController.m │ │ ├── TypingIndicatorView.h │ │ └── TypingIndicatorView.m │ ├── Messenger-Storyboard/ │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.xib │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── ViewController.h │ │ ├── ViewController.m │ │ └── main.m │ ├── Messenger-Swift/ │ │ ├── AppDelegate.swift │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.xib │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── MessageViewController.swift │ ├── Messenger-iPad-Sheet/ │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Info.plist │ │ └── main.m │ ├── Messenger.xcodeproj/ │ │ ├── project.pbxproj │ │ └── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── Messenger.xccheckout │ ├── Messenger.xcworkspace/ │ │ └── contents.xcworkspacedata │ ├── Podfile │ └── Pods/ │ ├── Local Podspecs/ │ │ └── SlackTextViewController.podspec.json │ ├── LoremIpsum/ │ │ ├── License.markdown │ │ ├── LoremIpsum/ │ │ │ ├── LoremIpsum.h │ │ │ └── LoremIpsum.m │ │ └── Readme.markdown │ ├── Pods.xcodeproj/ │ │ └── project.pbxproj │ └── Target Support Files/ │ ├── LoremIpsum/ │ │ ├── LoremIpsum-dummy.m │ │ ├── LoremIpsum-prefix.pch │ │ └── LoremIpsum.xcconfig │ ├── Pods/ │ │ ├── Pods-acknowledgements.markdown │ │ ├── Pods-acknowledgements.plist │ │ ├── Pods-dummy.m │ │ ├── Pods-frameworks.sh │ │ ├── Pods-resources.sh │ │ ├── Pods.debug.xcconfig │ │ └── Pods.release.xcconfig │ └── SlackTextViewController/ │ ├── SlackTextViewController-dummy.m │ ├── SlackTextViewController-prefix.pch │ └── SlackTextViewController.xcconfig ├── File Templates/ │ ├── SlackTextView Controller.xctemplate/ │ │ ├── TemplateInfo.plist │ │ ├── UICollectionView/ │ │ │ ├── ___FILEBASENAME___.h │ │ │ └── ___FILEBASENAME___.m │ │ └── UITableView/ │ │ ├── ___FILEBASENAME___.h │ │ └── ___FILEBASENAME___.m │ └── install.sh ├── Gemfile ├── LICENSE ├── README.md ├── SlackTextViewController/ │ ├── SlackTextViewController/ │ │ ├── Info.plist │ │ └── SlackTextViewController.h │ ├── SlackTextViewController.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── xcbaselines/ │ │ │ └── F5B4E78D1C4DA053005CBBE0.xcbaseline/ │ │ │ ├── F8445E9E-5B76-4616-90F9-7D94D0E1142F.plist │ │ │ └── Info.plist │ │ └── xcschemes/ │ │ ├── SlackTextViewController.xcscheme │ │ └── SlackTextViewControllerTests.xcscheme │ └── SlackTextViewControllerTests/ │ ├── FrameworkTests.m │ └── Info.plist ├── SlackTextViewController.podspec └── Source/ ├── SLKInputAccessoryView.h ├── SLKInputAccessoryView.m ├── SLKTextInput+Implementation.m ├── SLKTextInput.h ├── SLKTextInputbar.h ├── SLKTextInputbar.m ├── SLKTextView+SLKAdditions.h ├── SLKTextView+SLKAdditions.m ├── SLKTextView.h ├── SLKTextView.m ├── SLKTextViewController.h ├── SLKTextViewController.m ├── SLKTypingIndicatorProtocol.h ├── SLKTypingIndicatorView.h ├── SLKTypingIndicatorView.m ├── SLKUIConstants.h ├── UIResponder+SLKAdditions.h ├── UIResponder+SLKAdditions.m ├── UIScrollView+SLKAdditions.h ├── UIScrollView+SLKAdditions.m ├── UIView+SLKAdditions.h └── UIView+SLKAdditions.m ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Slack open source code of conduct ## Introduction Diversity and inclusion make our community strong. We encourage participation from the most varied and diverse backgrounds possible and want to be very clear about where we stand. Our goal is to maintain a safe, helpful and friendly community for everyone, regardless of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other defining characteristic. This code and related procedures also apply to unacceptable behavior occurring outside the scope of community activities, in all community venues (online and in-person) as well as in all one-on-one communications, and anywhere such behavior has the potential to adversely affect the safety and well-being of community members. ## Expected Behavior * Be welcoming. * Be kind. * Look out for each other. ## Unacceptable Behavior * Conduct or speech which might be considered sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory or offensive in nature. * Unwelcome, suggestive, derogatory or inappropriate nicknames or terms. * Disrespect towards others. (Jokes, innuendo, dismissive attitudes.) * Intimidation or harassment (online or in-person). Please read the [Citizen Code of Conduct](http://citizencodeofconduct.org/) for how we interpret harassment. * Disrespect towards differences of opinion. * Inappropriate attention or contact. Be aware of how your actions affect others. If it makes someone uncomfortable, stop. * Not understanding the differences between constructive criticism and disparagement. * Sustained disruptions. * Violence, threats of violence or violent language. ## Enforcement Understand that speech and actions have consequences, and unacceptable behavior will not be tolerated. If you are the subject of, or witness to any violations of this Code of Conduct, please contact us by submitting a form [here](https://docs.google.com/a/slack-corp.com/forms/d/1NVqj2S2Q49XVIOT5N3L6Tx1oihvk9CpMa_UX8T_6ESo/viewform), or email conduct@slack.com. If violations occur, organizers will take any action they deem appropriate for the infraction, up to and including expulsion. _Thanks to the [Django Code of Conduct](https://www.djangoproject.com/conduct/), [The Citizen Code of Conduct](http://citizencodeofconduct.org/), [The Rust Code of Conduct](https://www.rust-lang.org/conduct.html) and [The Ada Initiative](http://adainitiative.org/2014/02/18/howto-design-a-code-of-conduct-for-your-community/)._ ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing at Slack ![Header Image](contributing_header_slack.png) ## Before Contributing Before contributing, please read our [Code of Conduct](CODE_OF_CONDUCT.md). We take it very seriously, and expect that you will as well. ## New Issues Before opening a new issue, please consider: - Reading [the documentation](https://github.com/slackhq/SlackTextViewController/blob/master/README.md) and [the changelog](https://github.com/slackhq/SlackTextViewController/blob/master/CHANGELOG.md) first. - Searching for any related issues and avoid creating duplicated issues. - Adding details, diagnoses, screenshots or any type of useful information in existing issues, even if they are marked as closed. The team will still review it. - Trying out the examples [provided in this repository](https://github.com/slackhq/SlackTextViewController/tree/master/Examples). - Taking the time to think of a solution and [**open a pull request**](#new-pull-requests) for either improving the documentation, fixing a bug or suggesting a feature. - Finally, [**open an issue**](https://github.com/slackhq/SlackTextViewController/issues/new) to report a bug, ask for help or suggest a feature. The more information you give, the better people can help you. ## New Pull Requests We love pull requests and we are generally very receptive to contributions. Things to keep in mind: - [Fork the repository](https://github.com/slackhq/SlackTextViewController) and make sure to work on a branch up to date with origin master. - Do your thing! - Be mindful about doing atomic commits, adding documentation to your changes, not refactoring too much. - Add tests covering the new code or functionality you are adding. - Add a descriptive title and add any useful information for the reviewer. If your contribution is a user facing thing, please attach a screenshot and/or screencast (gif preferably). - Read and agree to our [Contributor License Agreement (CLA)](https://docs.google.com/a/slack-corp.com/forms/d/1q_w8rlJG_x_xJOoSUMNl7R35rkpA7N6pUkKhfHHMD9c/viewform). _We cannot accept your PR without your agreement to our CLA_. - Create your pull request (yay!). If it is in relation to an existing issue, please mention it on the title or description. [Interested in knowing more about about pull requests at Slack?](https://slack.engineering/on-empathy-pull-requests-979e4257d158#.awxtvmb2z) ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ * [ ] I've read and understood the [Contributing guidelines](https://github.com/slackhq/SlackTextViewController/blob/master/.github/CONTRIBUTING.md) and have done my best effort to follow them. * [ ] I've read and agree to the [Code of Conduct](https://github.com/slackhq/SlackTextViewController/blob/master/.github/CODE_OF_CONDUCT.md). * [ ] I've searched for any related issues and avoided creating a duplicate issue. #### Description > e.g. Description of the bug or feature #### Reproducible in: * [ ] This is reproducible in the sample project. SlackTextViewController version: iOS version(s): Device(s): #### Steps to reproduce: 1. 2. 3. ### Expected result: > e.g. What you expected to happen ### Actual result: > e.g. What actually happened ### Attachments: > e.g. Logs, screenshots, screencast, sample project, funny gif, etc. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ * [ ] I've read and understood the [Contributing guidelines](https://github.com/slackhq/SlackTextViewController/blob/master/.github/CONTRIBUTING.md) and have done my best effort to follow them. * [ ] I've read and agree to the [Code of Conduct](https://github.com/slackhq/SlackTextViewController/blob/master/.github/CODE_OF_CONDUCT.md). * [ ] I've been mindful about doing atomic commits, adding documentation to my changes, not refactoring too much. * [ ] I've added a descriptive title and added any useful information for the reviewer. Where appropriate, I've attached a screenshot and/or screencast (gif preferrably). * [ ] I've listed my changes on the [Changelog(https://github.com/slackhq/SlackTextViewController/blob/master/CHANGELOG.md) file. * [ ] I've read, agree to, and signed the [Contributor License Agreement (CLA)](https://docs.google.com/a/slack-corp.com/forms/d/1q_w8rlJG_x_xJOoSUMNl7R35rkpA7N6pUkKhfHHMD9c/viewform). #### PR Summary > e.g. New functionality for producing whatsits. #### Related Issues > e.g. Fixes #206 and closes #230 #### Test strategy > e.g. Add tests around whatsit production. ================================================ FILE: .gitignore ================================================ # Xcode .DS_Store */build/* *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata profile *.moved-aside DerivedData .idea/ *.hmap .gutter.json Carthage/ SlackTextViewController/Pods SlackTextViewController/Podfile.lock SlackTextViewController/*.xcworkspace ================================================ FILE: .travis.yml ================================================ osx_image: xcode7 language: objective-c cache: - bundler - cocoapods before_install: - bundle install - cd SlackTextViewController && pod install && cd $TRAVIS_BUILD_DIR script: - xctool test notifications: slack: - secure: DOv3x21M1pnyrZ5jT5bZyHG0lg2E2tnj+6TVNHUD2ZYjab3X9oYz6PzL0YaC0M2S0rtiJtjO7jd1hdXHPg9CTUg04oGLSWe43Fmt2WAprszSXwyvNnqHNilBHFwhuM7MqcbV1uu3R/5CZVuHA0MKZqLQtDx2N4ep/5Lg6ehmuoA= after_success: - bundle exec slather ================================================ FILE: .xctool-args ================================================ [ "-workspace", "SlackTextViewController/SlackTextViewController.xcworkspace", "-scheme", "SnapshotTests", "-sdk", "iphonesimulator", "-configuration", "Debug", "-destination", "name=iPhone 6s,OS=9.2", ] ================================================ FILE: CHANGELOG.md ================================================ # Change Log ## [Version 1.9.7](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.9.7) #### Features: - Enables building the CocoaPod with `CLANG_MODULES_ENABLED=NO` which enables compiling with `ccache`. ## [Version 1.9.6](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.9.6) This release includes many iOS 11 and iPhone X hot fixes. ##### Features: - Allowing color customization in typing indicator. By @dskatz22 (#613) - Adding support for an optional view to host outlets under the text view. By @dzenbot (#562) - Exposing auto-completion variables. By @dzenbot (#561) ##### Hot Fixes & Enhancements: - Fixed the text input not being interactive on iOS 11. By @dzenbot (#624) - Fixed iPhone X issue where the text input bar wouldn't expand to the bottom of the screen, below the home indicator. (#619) - Fixed scroll view content inset adjustments on iOS 11. By @gim- (#643) - Fixed compiler error in example app. By @BasThomas (#629) - Fixed content offset for keyboard when uninverted. By @ZAndyL (#542) ##### Deprecation: - Deprecated `-shouldProcessTextForAutoCompletion:` in favor of `-shouldProcessTextForAutoCompletion` ##### CI: - Building the SlackTextViewController framework and running basic tests using BuddyBuild CI, against all PRs. By @dzenbot (#640) ## [Version 1.9.5](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.9.5) ##### Features: - Migrated the library to be using NSAttributedString underneath. The `text` property on `SLKTextView` uses a NSAttributedString representation based on its font and text color, and doesn't forward to super. By @jacywu07 (#501) - As part of the migration to NSAttributedString, new helpers for attributed strings have been added to `SLKTextView+SLKAdditions`. - Introduced a new API to end users to open the auto-completion mode with a given prefix. By @jacywu07 (#506) - Exposed the private `cacheTextView` method. By @acandelaria1 (#513) ##### Hot Fixes & Enhancements: - Updated the sample project to Swift 3! By @cyhsutw (#522) - Added a property to allow the user to set how many lines of text SLKTextView's placeholder should have. By @jedmund (#505) - Tweaked keyboard height calculations on invert mode. By @ZAndyL (#512) ## [Version 1.9.4](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.9.4) ##### Hot Fixes & Enhancements: - Fixed keyboard status updates inconsistencies, causing sometimes the text input bar not to follow the keyboard. - Fixed bottom margin inconsistencies. Thanks @yury! 💪 - Fixed an edge case where the caret would jump to the end after double-space completion in middle of text. Thanks @mtackes - Improved Carthage support ## [Version 1.9.3](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.9.3) ##### Hot Fixes & Enhancements: - Fixes a regression causing to trigger auto-completion text processing even if no prefix have been registered. This was causing a crash to many. Sorry about that! - Ignores keyboard notifications when no valid first responder is detected. This fixes the text input not following the keyboard at times. - Now `shouldProcessTextForAutoCompletion:` requires calling super. ## [Version 1.9.2](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.9.2) #### SlackTextViewController is now MIT licensed! ##### Features & Enhancements: - Better Swift 2.2 support with nullability annotations and shiny new Swift sample code. You rock @weijentu 🙇 - Introduced `SLKTextInput` protocol: A `UITextInput` extension to decouple all the text processing features related to auto-completion, to reuse in any text component such as UISearchBar, UITextField, UITextView, etc. - Added a new API `-shouldProcessTextForAutoCompletion` to be able to opt-out from text processing for auto-completion. - The `registeredPrefixes` property are now of type NSSet (instead of NSArray). - Added animation to views when switching from a keyboard to a custom input view. Thanks @cyhsutw! - Made `keyboardStatus` public, making it easier to check for the current keyboard state. ##### Hot Fixes: - Fixed a use case where the textInput would not follow the keyboard when dismissing. - Improved text caching from the textInput, specially when moving the app to the background (in case the app crashes while being on the background). - Fixed misaligned placeholder labels in the textView and out of bounds. - Fixed the textInput not growing accordantly to the font size. This was a regressions since version `1.7.1` - Many, many, many auto-completion bug fixes 💪 ## [Version 1.9.1](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.9.1) ##### Features & Enhancements: - Renamed public `autoCompleteFormatting` to `formattingEnabled`. - Doesn't opt-out anymore from the built-in menu menu items **Define**, **Replace** and **Share**. - Made the collectionView's default background color to white. - The auto-completion view is now presented above of the table/collection view avoiding to push it up/down every time. Much better UX! - Stored all key commands in a instance variable, to avoid recreating the array at every character update. ##### Hot Fixes: - Fixed library from not compiling on iOS 8. - Fixed the right button from stretching when animating the constrains. This was broken since iOS 9. - Improved `UITabBar` support by considering `hidesBottomBarWhenPushed` too. - Fixed crash when double tapping the space bar while the textView was empty. - Fixed issue causing not to forward all UITextViewDelegate callbacks. ## [Version 1.9](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.9) ##### Deprecations: - Deprecated the keyboard panning gesture on iOS 9, to drag the keyboard up and down. More information about this in #361 - Deprecated `isLoupeVisible`, which will cause issues when auto-completion mode is active and moving the cursor of the text view. More information about this in #361 ##### Features & Enhancements: - Enabled interaction while the right button and the auto-completion view are being presented animatedly - Does not scroll to the bottom anymore, if the content size is smaller than its bounds, when `shouldScrollToBottomAfterKeyboardShows` is enabled. ##### Hot Fixes: - Fixed the keyboard status and custom notifications not being set on the right order. - Fixed a crash when calling unrecognised selectors internally in the `UITextViewDelegate` method implementation, when using other subclasses of UITextView. - Fixed `UITabBar` support. This was a regression. Thanks @LHIOUI for the headsup 👊 - Fixed cursor dragging issues when deep pressing on the keyboard's trackpad. - Fixed the auto-completion layout being busted in 1.8. This was a regression. ## [Version 1.8](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.8) ##### Features & Enhancements: * Added [Markdown Formatting](https://github.com/slackhq/SlackTextViewController#markdown-formatting) ⚡️📝, a useful and simple way to allow your users to auto-complete any markdown formatting from within the text input. 1 small step to make writing markdown quicker. * The shake gesture now presents an `UIAlertController` for iOS 8 and above. Still supports the old and good `UIAlertView` for legacy versions. ##### Hot Fixes: * Fixed some content inset non-sense * Scrolling to top now really scrolls to top. Not down. Oopsie. * Scrolling down when the keyboard gets presented is also working now. Yay :tada: ! * The placeholder font now matches the textView font, for real this time. * Avoids reloading the text view when there is [no key for cache](https://github.com/slackhq/SlackTextViewController/commit/d3730e2a880c9fd8768623f923d5443432829ee9). Thanks @susieyy! * Removed annoying iOS 8 warnings. ## [Version 1.7.2](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.7.2) ##### Hot Fixes: * Better [Carthage](https://github.com/Carthage/Carthage) support 🙏 * Fixed the textInput's [right margin not being honored or honoured](https://github.com/slackhq/SlackTextViewController/commit/6ed6b29f3a82ef22b626eda08dfe57ec4ab37df1). Thanks @ikesyo 🙌 * Fixed the textView's contentSize to never be higher than its bounds. Very useful for stuff. ## [Version 1.7.1](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.7.1) ##### Features & Enhancements: * Changed how the auto-completion API worked: it now allows asynch auto-completion! Have a look at how to set it up / update it](https://github.com/slackhq/SlackTextViewController#autocompletion) * Added keyboard trackpad detection for iOS 9. Used internally for disables auto-completion while its detected, so we avoid crazy things! 👻 * Improved the magnifying glass detection. * Gonna get a nice warning to remind you to `super` in `viewDidLoad` now. Fancy! * Slowed down bouncy animation by 0.15 seconds * Disabled `cellLayoutMarginsFollowReadableWidth` on iOS 9 for the auto-completion view. No need for large margins, come on! ##### Hot Fixes: * Fixed compatibility issues with [Cocoapods](https://github.com/CocoaPods/CocoaPods) 0.39.0 new requirements. All sources are now in the same root level. * Fixed keyboard presentation when pushing a view controller instance. Thanks @fastred! * Fixed auto-layout issues on the Edit Mode. * Fixed `maximumHeightForAutoCompletionView` calculations. Maths! * Fixed [crash caused by calling `layoutIfNeeded` too early](https://github.com/slackhq/SlackTextViewController/commit/dceedc70393e873d70c82da39c9f2cc9f18fda5a) * [Removed duplicated declarations](https://github.com/slackhq/SlackTextViewController/commit/f61e4e2cbc03ff30c9391fc86eba2c5ba8674f47), specially causing nightmares to Swifters. * Removed unused internal methods. * Better (Carthage)[https://github.com/Carthage/Carthage] support 🙏 ## [Version 1.7](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.7) ##### Deprecations: - Removed `shouldForceTextInputbarAdjustment` and replace it with `-forceTextInputbarAdjustmentForResponder:` - Renamed `canShowTypeIndicator` and replace it with `canShowTypingIndicator` - Renamed `editortLeftButton` with `editorLeftButton`, and `editortRightButton` with `editorRightButton` ##### Features & enhancements: - Added iOS 9 (beta 5) support, with fixes for multi-tasking on iPad and external keyboard shortcut hud support, and many small layout tweaks. - Added the ability to show/hide the text input bar, with animation support, using `setTextInputbarHidden:animated:`. Thanks @aryaxt! - Added better Accessibility support with [Dynamic Type](https://github.com/slackhq/SlackTextViewController#dynamic-type) - [Improved the keyboard panning gesture by dragging the text input bar from the bottom](https://cloud.githubusercontent.com/assets/590579/9448678/5423f254-4a74-11e5-870d-80c377d24937.gif) (feature flagged as it needs more testing) - Added 2 more `UIScrollViewDelegate` method declarations to SLKTextViewController's header. `super` is required! ##### Hot Fixes: - Fixed wrong auto-completion view height calculations. - Fixed a very bad retain cycle reported in #234 - Fixed the keyboard view detection on iOS 9 - Fixed swift compiler warning. Thanks @csjones ## [Version 1.6](https://github.com/slackhq/SlackTextViewController/releases/tag/v1.6) ##### Features: - Added support for custom typing indicator, following the same pattern of registering a class using `registerClassForTypingIndicatorView:`, while this class conforms to `SLKTypingIndicatorProtocol`. Please refer to the documentation for more details about the feature. Thanks @sveinhal! (#207) - Added support for registering longer auto-completion prefixes - Improved drastically the keyboard panning experience, making it much more smooth now. Awesome stuff @camitox! - Added the ability to ignore the text inputbar adjustment when the keyboard is presented, using `ignoreTextInputbarAdjustment`. This is generally useful when SLKTVC is used in a custom modal presentation and when you want to manipulate the view's alignment yourself. ##### Hot Fixes: - No longer overriding the default background color of UITableView. Oupsi! (#205) - Made sure not to register the same notifications twice. - Fixes issue where the text input would not adjust on top of the keyboard when presenting an `UIAlertViewController` (`UIAlertView` or `UIActionSheet`) in iOS8 ## Previous versions For more release notes of this project, please visit https://github.com/slackhq/SlackTextViewController/releases ================================================ FILE: Examples/Messenger-Programatic/AppDelegate.h ================================================ // // AppDelegate.h // Messenger-Programatic // // Created by Ignacio Romero Zurbuchen on 8/15/14. // Copyright (c) 2014 Slack Technologies, Inc. All rights reserved. // #import @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @end ================================================ FILE: Examples/Messenger-Programatic/AppDelegate.m ================================================ // // AppDelegate.m // Messenger-Programatic // // Created by Ignacio Romero Zurbuchen on 8/15/14. // Copyright (c) 2014 Slack Technologies, Inc. All rights reserved. // #import "AppDelegate.h" #import "SLKTextViewController.h" #import "MessageViewController.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[MessageViewController new]]; [self.window makeKeyAndVisible]; return YES; } @end ================================================ FILE: Examples/Messenger-Programatic/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName Messenger CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance ================================================ FILE: Examples/Messenger-Programatic/main.m ================================================ // // main.m // Messenger-Programatic // // Created by Ignacio Romero Zurbuchen on 8/15/14. // Copyright (c) 2014 Slack Technologies, Inc. All rights reserved. // #import #import "AppDelegate.h" int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ================================================ FILE: Examples/Messenger-Shared/Bridge-Header.h ================================================ // // Use this file to import your target's public headers that you would like to expose to Swift. // #import #import "Message.h" #import "MessageTableViewCell.h" #import "MessageTextView.h" #import "TypingIndicatorView.h" #import ================================================ FILE: Examples/Messenger-Shared/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "AppIcon-29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "AppIcon-29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "AppIcon-40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "AppIcon-40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "AppIcon-60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "AppIcon-60@3x.png", "scale" : "3x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "1x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "AppIcon-29~ipad.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "AppIcon-29@2x~ipad.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "AppIcon-40~ipad.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "AppIcon-40@2x~ipad.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "AppIcon-76~ipad.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "AppIcon-76@2x~ipad.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "AppIcon-83.5@2x.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Messenger-Shared/Images.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Messenger-Shared/Images.xcassets/Icons/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Messenger-Shared/Images.xcassets/Icons/icn_append.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icn_append.pdf" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: Examples/Messenger-Shared/Images.xcassets/Icons/icn_arrow_down.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icn_arrow_down.pdf", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Messenger-Shared/Images.xcassets/Icons/icn_arrow_up.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icn_arrow_up.pdf", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Messenger-Shared/Images.xcassets/Icons/icn_editing.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icn_editing.pdf" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: Examples/Messenger-Shared/Images.xcassets/Icons/icn_pic.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icn_pic.pdf" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: Examples/Messenger-Shared/Images.xcassets/Icons/icn_typing.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icn_typing.pdf" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: Examples/Messenger-Shared/Images.xcassets/Icons/icn_upload.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icn_upload.pdf" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: Examples/Messenger-Shared/Images.xcassets/LaunchImage.launchimage/Contents.json ================================================ { "images" : [ { "extent" : "full-screen", "idiom" : "iphone", "subtype" : "736h", "filename" : "Default-hd-plus@2x.png", "minimum-system-version" : "8.0", "orientation" : "portrait", "scale" : "3x" }, { "extent" : "full-screen", "idiom" : "iphone", "subtype" : "736h", "filename" : "Default-hd-plus@2x~landscape.png", "minimum-system-version" : "8.0", "orientation" : "landscape", "scale" : "3x" }, { "extent" : "full-screen", "idiom" : "iphone", "subtype" : "667h", "filename" : "Default-hd@2x.png", "minimum-system-version" : "8.0", "orientation" : "portrait", "scale" : "2x" }, { "orientation" : "portrait", "idiom" : "iphone", "extent" : "full-screen", "minimum-system-version" : "7.0", "filename" : "Default@2x.png", "scale" : "2x" }, { "extent" : "full-screen", "idiom" : "iphone", "subtype" : "retina4", "filename" : "Default-568@2x.png", "minimum-system-version" : "7.0", "orientation" : "portrait", "scale" : "2x" }, { "orientation" : "portrait", "idiom" : "ipad", "extent" : "full-screen", "minimum-system-version" : "7.0", "scale" : "1x" }, { "orientation" : "landscape", "idiom" : "ipad", "extent" : "full-screen", "minimum-system-version" : "7.0", "scale" : "1x" }, { "orientation" : "portrait", "idiom" : "ipad", "extent" : "full-screen", "minimum-system-version" : "7.0", "scale" : "2x" }, { "orientation" : "landscape", "idiom" : "ipad", "extent" : "full-screen", "minimum-system-version" : "7.0", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Messenger-Shared/Message.h ================================================ // // Message.h // Messenger // // Created by Ignacio Romero Z. on 1/16/15. // Copyright (c) 2015 Slack Technologies, Inc. All rights reserved. // #import #import @interface Message : NSObject @property (nonatomic, strong) NSString *username; @property (nonatomic, strong) NSString *text; @end ================================================ FILE: Examples/Messenger-Shared/Message.m ================================================ // // Message.m // Messenger // // Created by Ignacio Romero Z. on 1/16/15. // Copyright (c) 2015 Slack Technologies, Inc. All rights reserved. // #import "Message.h" @implementation Message @end ================================================ FILE: Examples/Messenger-Shared/MessageTableViewCell.h ================================================ // // MessageTableViewCell.h // Messenger // // Created by Ignacio Romero Zurbuchen on 9/1/14. // Copyright (c) 2014 Slack Technologies, Inc. All rights reserved. // #import static CGFloat kMessageTableViewCellMinimumHeight = 50.0; static CGFloat kMessageTableViewCellAvatarHeight = 30.0; static NSString *MessengerCellIdentifier = @"MessengerCell"; static NSString *AutoCompletionCellIdentifier = @"AutoCompletionCell"; @interface MessageTableViewCell : UITableViewCell @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UILabel *bodyLabel; @property (nonatomic, strong) UIImageView *thumbnailView; @property (nonatomic, strong) NSIndexPath *indexPath; @property (nonatomic) BOOL usedForMessage; + (CGFloat)defaultFontSize; @end ================================================ FILE: Examples/Messenger-Shared/MessageTableViewCell.m ================================================ // // MessageTableViewCell.m // Messenger // // Created by Ignacio Romero Zurbuchen on 9/1/14. // Copyright (c) 2014 Slack Technologies, Inc. All rights reserved. // #import "MessageTableViewCell.h" #import "SLKUIConstants.h" @implementation MessageTableViewCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { self.selectionStyle = UITableViewCellSelectionStyleNone; self.backgroundColor = [UIColor whiteColor]; [self configureSubviews]; } return self; } - (void)configureSubviews { [self.contentView addSubview:self.thumbnailView]; [self.contentView addSubview:self.titleLabel]; [self.contentView addSubview:self.bodyLabel]; NSDictionary *views = @{@"thumbnailView": self.thumbnailView, @"titleLabel": self.titleLabel, @"bodyLabel": self.bodyLabel, }; NSDictionary *metrics = @{@"tumbSize": @(kMessageTableViewCellAvatarHeight), @"padding": @15, @"right": @10, @"left": @5 }; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[thumbnailView(tumbSize)]-right-[titleLabel(>=0)]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[thumbnailView(tumbSize)]-right-[bodyLabel(>=0)]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[thumbnailView(tumbSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; if ([self.reuseIdentifier isEqualToString:MessengerCellIdentifier]) { [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(20)]-left-[bodyLabel(>=0@999)]-left-|" options:0 metrics:metrics views:views]]; } else { [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[titleLabel]|" options:0 metrics:metrics views:views]]; } } - (void)prepareForReuse { [super prepareForReuse]; self.selectionStyle = UITableViewCellSelectionStyleNone; CGFloat pointSize = [MessageTableViewCell defaultFontSize]; self.titleLabel.font = [UIFont boldSystemFontOfSize:pointSize]; self.bodyLabel.font = [UIFont systemFontOfSize:pointSize]; self.titleLabel.text = @""; self.bodyLabel.text = @""; } #pragma mark - Getters - (UILabel *)titleLabel { if (!_titleLabel) { _titleLabel = [UILabel new]; _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; _titleLabel.backgroundColor = [UIColor clearColor]; _titleLabel.userInteractionEnabled = NO; _titleLabel.numberOfLines = 0; _titleLabel.textColor = [UIColor grayColor]; _titleLabel.font = [UIFont boldSystemFontOfSize:[MessageTableViewCell defaultFontSize]]; } return _titleLabel; } - (UILabel *)bodyLabel { if (!_bodyLabel) { _bodyLabel = [UILabel new]; _bodyLabel.translatesAutoresizingMaskIntoConstraints = NO; _bodyLabel.backgroundColor = [UIColor clearColor]; _bodyLabel.userInteractionEnabled = NO; _bodyLabel.numberOfLines = 0; _bodyLabel.textColor = [UIColor darkGrayColor]; _bodyLabel.font = [UIFont systemFontOfSize:[MessageTableViewCell defaultFontSize]]; } return _bodyLabel; } - (UIImageView *)thumbnailView { if (!_thumbnailView) { _thumbnailView = [UIImageView new]; _thumbnailView.translatesAutoresizingMaskIntoConstraints = NO; _thumbnailView.userInteractionEnabled = NO; _thumbnailView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0]; _thumbnailView.layer.cornerRadius = kMessageTableViewCellAvatarHeight/2.0; _thumbnailView.layer.masksToBounds = YES; } return _thumbnailView; } + (CGFloat)defaultFontSize { CGFloat pointSize = 16.0; NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory); return pointSize; } @end ================================================ FILE: Examples/Messenger-Shared/MessageTextView.h ================================================ // // MessageTextView.h // Messenger // // Created by Ignacio Romero Z. on 1/20/15. // Copyright (c) 2015 Slack Technologies, Inc. All rights reserved. // #import "SLKTextView.h" @interface MessageTextView : SLKTextView @end ================================================ FILE: Examples/Messenger-Shared/MessageTextView.m ================================================ // // MessageTextView.m // Messenger // // Created by Ignacio Romero Z. on 1/20/15. // Copyright (c) 2015 Slack Technologies, Inc. All rights reserved. // #import "MessageTextView.h" @implementation MessageTextView - (instancetype)init { if (self = [super init]) { // Do something } return self; } - (void)willMoveToSuperview:(UIView *)newSuperview { [super willMoveToSuperview:newSuperview]; self.backgroundColor = [UIColor whiteColor]; self.placeholder = NSLocalizedString(@"Message", nil); self.placeholderColor = [UIColor lightGrayColor]; self.pastableMediaTypes = SLKPastableMediaTypeAll; self.layer.borderColor = [UIColor colorWithRed:217.0/255.0 green:217.0/255.0 blue:217.0/255.0 alpha:1.0].CGColor; } @end ================================================ FILE: Examples/Messenger-Shared/MessageViewController.h ================================================ // // MessageViewController.h // Messenger // // Created by Ignacio Romero Zurbuchen on 8/15/14. // Copyright (c) 2014 Slack Technologies, Inc. All rights reserved. // #import "SLKTextViewController.h" @interface MessageViewController : SLKTextViewController @end ================================================ FILE: Examples/Messenger-Shared/MessageViewController.m ================================================ // // MessageViewController.m // Messenger // // Created by Ignacio Romero Zurbuchen on 8/15/14. // Copyright (c) 2014 Slack Technologies, Inc. All rights reserved. // #import "MessageViewController.h" #import "MessageTableViewCell.h" #import "MessageTextView.h" #import "TypingIndicatorView.h" #import "Message.h" #import #define DEBUG_CUSTOM_TYPING_INDICATOR 0 #define DEBUG_CUSTOM_BOTTOM_VIEW 0 @interface MessageViewController () @property (nonatomic, strong) NSMutableArray *messages; @property (nonatomic, strong) NSArray *users; @property (nonatomic, strong) NSArray *channels; @property (nonatomic, strong) NSArray *emojis; @property (nonatomic, strong) NSArray *commands; @property (nonatomic, strong) NSArray *searchResult; @property (nonatomic, strong) UIWindow *pipWindow; @property (nonatomic, weak) Message *editingMessage; @end @implementation MessageViewController - (instancetype)init { self = [super initWithTableViewStyle:UITableViewStylePlain]; if (self) { [self commonInit]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self commonInit]; } return self; } + (UITableViewStyle)tableViewStyleForCoder:(NSCoder *)decoder { return UITableViewStylePlain; } - (void)commonInit { [[NSNotificationCenter defaultCenter] addObserver:self.tableView selector:@selector(reloadData) name:UIContentSizeCategoryDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputbarDidMove:) name:SLKTextInputbarDidMoveNotification object:nil]; // Register a SLKTextView subclass, if you need any special appearance and/or behavior customisation. [self registerClassForTextView:[MessageTextView class]]; #if DEBUG_CUSTOM_TYPING_INDICATOR // Register a UIView subclass, conforming to SLKTypingIndicatorProtocol, to use a custom typing indicator view. [self registerClassForTypingIndicatorView:[TypingIndicatorView class]]; #endif } #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Example's configuration [self configureDataSource]; [self configureActionItems]; // SLKTVC's configuration self.bounces = YES; self.shakeToClearEnabled = YES; self.keyboardPanningEnabled = YES; self.shouldScrollToBottomAfterKeyboardShows = NO; self.inverted = YES; [self.leftButton setImage:[UIImage imageNamed:@"icn_upload"] forState:UIControlStateNormal]; [self.leftButton setTintColor:[UIColor grayColor]]; [self.rightButton setTitle:NSLocalizedString(@"Send", nil) forState:UIControlStateNormal]; self.textInputbar.autoHideRightButton = YES; self.textInputbar.maxCharCount = 256; self.textInputbar.counterStyle = SLKCounterStyleSplit; self.textInputbar.counterPosition = SLKCounterPositionTop; [self.textInputbar.editorTitle setTextColor:[UIColor darkGrayColor]]; [self.textInputbar.editorLeftButton setTintColor:[UIColor colorWithRed:0.0/255.0 green:122.0/255.0 blue:255.0/255.0 alpha:1.0]]; [self.textInputbar.editorRightButton setTintColor:[UIColor colorWithRed:0.0/255.0 green:122.0/255.0 blue:255.0/255.0 alpha:1.0]]; #if DEBUG_CUSTOM_BOTTOM_VIEW // Example of view that can be added to the bottom of the text view UIView *bannerView = [UIView new]; bannerView.translatesAutoresizingMaskIntoConstraints = NO; bannerView.backgroundColor = [UIColor blueColor]; NSDictionary *views = NSDictionaryOfVariableBindings(bannerView); [self.textInputbar.contentView addSubview:bannerView]; [self.textInputbar.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[bannerView]|" options:0 metrics:nil views:views]]; [self.textInputbar.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[bannerView(40)]|" options:0 metrics:nil views:views]]; #endif #if !DEBUG_CUSTOM_TYPING_INDICATOR self.typingIndicatorView.canResignByTouch = YES; #endif self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; [self.tableView registerClass:[MessageTableViewCell class] forCellReuseIdentifier:MessengerCellIdentifier]; [self.autoCompletionView registerClass:[MessageTableViewCell class] forCellReuseIdentifier:AutoCompletionCellIdentifier]; [self registerPrefixesForAutoCompletion:@[@"@", @"#", @":", @"+:", @"/"]]; [self.textView registerMarkdownFormattingSymbol:@"*" withTitle:@"Bold"]; [self.textView registerMarkdownFormattingSymbol:@"_" withTitle:@"Italics"]; [self.textView registerMarkdownFormattingSymbol:@"~" withTitle:@"Strike"]; [self.textView registerMarkdownFormattingSymbol:@"`" withTitle:@"Code"]; [self.textView registerMarkdownFormattingSymbol:@"```" withTitle:@"Preformatted"]; [self.textView registerMarkdownFormattingSymbol:@">" withTitle:@"Quote"]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; } #pragma mark - Example's Configuration - (void)configureDataSource { NSMutableArray *array = [[NSMutableArray alloc] init]; for (int i = 0; i < 100; i++) { NSInteger words = (arc4random() % 40)+1; Message *message = [Message new]; message.username = [LoremIpsum name]; message.text = [LoremIpsum wordsWithNumber:words]; [array addObject:message]; } NSArray *reversed = [[array reverseObjectEnumerator] allObjects]; self.messages = [[NSMutableArray alloc] initWithArray:reversed]; self.users = @[@"Allen", @"Anna", @"Alicia", @"Arnold", @"Armando", @"Antonio", @"Brad", @"Catalaya", @"Christoph", @"Emerson", @"Eric", @"Everyone", @"Steve"]; self.channels = @[@"General", @"Random", @"iOS", @"Bugs", @"Sports", @"Android", @"UI", @"SSB"]; self.emojis = @[@"-1", @"m", @"man", @"machine", @"block-a", @"block-b", @"bowtie", @"boar", @"boat", @"book", @"bookmark", @"neckbeard", @"metal", @"fu", @"feelsgood"]; self.commands = @[@"msg", @"call", @"text", @"skype", @"kick", @"invite"]; } - (void)configureActionItems { UIBarButtonItem *arrowItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"icn_arrow_down"] style:UIBarButtonItemStylePlain target:self action:@selector(hideOrShowTextInputbar:)]; UIBarButtonItem *editItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"icn_editing"] style:UIBarButtonItemStylePlain target:self action:@selector(editRandomMessage:)]; UIBarButtonItem *typeItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"icn_typing"] style:UIBarButtonItemStylePlain target:self action:@selector(simulateUserTyping:)]; UIBarButtonItem *appendItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"icn_append"] style:UIBarButtonItemStylePlain target:self action:@selector(fillWithText:)]; UIBarButtonItem *pipItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"icn_pic"] style:UIBarButtonItemStylePlain target:self action:@selector(togglePIPWindow:)]; self.navigationItem.rightBarButtonItems = @[arrowItem, pipItem, editItem, appendItem, typeItem]; } #pragma mark - Action Methods - (void)hideOrShowTextInputbar:(id)sender { BOOL hide = !self.textInputbarHidden; UIImage *image = hide ? [UIImage imageNamed:@"icn_arrow_up"] : [UIImage imageNamed:@"icn_arrow_down"]; UIBarButtonItem *buttonItem = (UIBarButtonItem *)sender; [self setTextInputbarHidden:hide animated:YES]; [buttonItem setImage:image]; } - (void)fillWithText:(id)sender { if (self.textView.text.length == 0) { int sentences = (arc4random() % 4); if (sentences <= 1) sentences = 1; self.textView.text = [LoremIpsum sentencesWithNumber:sentences]; } else { [self.textView slk_insertTextAtCaretRange:[NSString stringWithFormat:@" %@", [LoremIpsum word]]]; } } - (void)simulateUserTyping:(id)sender { if ([self canShowTypingIndicator]) { #if DEBUG_CUSTOM_TYPING_INDICATOR __block TypingIndicatorView *view = (TypingIndicatorView *)self.typingIndicatorProxyView; CGFloat scale = [UIScreen mainScreen].scale; CGSize imgSize = CGSizeMake(kTypingIndicatorViewAvatarHeight*scale, kTypingIndicatorViewAvatarHeight*scale); // This will cause the typing indicator to show after a delay ¯\_(ツ)_/¯ [LoremIpsum asyncPlaceholderImageWithSize:imgSize completion:^(UIImage *image) { UIImage *thumbnail = [UIImage imageWithCGImage:image.CGImage scale:scale orientation:UIImageOrientationUp]; [view presentIndicatorWithName:[LoremIpsum name] image:thumbnail]; }]; #else [self.typingIndicatorView insertUsername:[LoremIpsum name]]; #endif } } - (void)didLongPressCell:(UIGestureRecognizer *)gesture { if (gesture.state != UIGestureRecognizerStateBegan) { return; } #ifdef __IPHONE_8_0 if (SLK_IS_IOS8_AND_HIGHER && [UIAlertController class]) { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; alertController.modalPresentationStyle = UIModalPresentationPopover; alertController.popoverPresentationController.sourceView = gesture.view.superview; alertController.popoverPresentationController.sourceRect = gesture.view.frame; [alertController addAction:[UIAlertAction actionWithTitle:@"Edit Message" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [self editCellMessage:gesture]; }]]; [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:NULL]]; [self.navigationController presentViewController:alertController animated:YES completion:nil]; } else { [self editCellMessage:gesture]; } #else [self editCellMessage:gesture]; #endif } - (void)editCellMessage:(UIGestureRecognizer *)gesture { MessageTableViewCell *cell = (MessageTableViewCell *)gesture.view; self.editingMessage = self.messages[cell.indexPath.row]; [self editText:self.editingMessage.text]; [self.tableView scrollToRowAtIndexPath:cell.indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES]; } - (void)editRandomMessage:(id)sender { int sentences = (arc4random() % 10); if (sentences <= 1) sentences = 1; [self editText:[LoremIpsum sentencesWithNumber:sentences]]; } - (void)editLastMessage:(id)sender { if (self.textView.text.length > 0) { return; } NSInteger lastSectionIndex = [self.tableView numberOfSections]-1; NSInteger lastRowIndex = [self.tableView numberOfRowsInSection:lastSectionIndex]-1; Message *lastMessage = [self.messages objectAtIndex:lastRowIndex]; [self editText:lastMessage.text]; [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:lastRowIndex inSection:lastSectionIndex] atScrollPosition:UITableViewScrollPositionBottom animated:YES]; } - (void)togglePIPWindow:(id)sender { if (!_pipWindow) { [self showPIPWindow:sender]; } else { [self hidePIPWindow:sender]; } } - (void)showPIPWindow:(id)sender { CGRect frame = CGRectMake(CGRectGetWidth(self.view.frame) - 60.0, 0.0, 50.0, 50.0); frame.origin.y = CGRectGetMinY(self.textInputbar.frame) - 60.0; _pipWindow = [[UIWindow alloc] initWithFrame:frame]; _pipWindow.backgroundColor = [UIColor blackColor]; _pipWindow.layer.cornerRadius = 10.0; _pipWindow.layer.masksToBounds = YES; _pipWindow.hidden = NO; _pipWindow.alpha = 0.0; [[UIApplication sharedApplication].keyWindow addSubview:_pipWindow]; [UIView animateWithDuration:0.25 animations:^{ _pipWindow.alpha = 1.0; }]; } - (void)hidePIPWindow:(id)sender { [UIView animateWithDuration:0.3 animations:^{ _pipWindow.alpha = 0.0; } completion:^(BOOL finished) { _pipWindow.hidden = YES; _pipWindow = nil; }]; } - (void)textInputbarDidMove:(NSNotification *)note { if (!_pipWindow) { return; } CGRect frame = self.pipWindow.frame; frame.origin.y = [note.userInfo[@"origin"] CGPointValue].y - 60.0; self.pipWindow.frame = frame; } #pragma mark - Overriden Methods - (BOOL)ignoreTextInputbarAdjustment { return [super ignoreTextInputbarAdjustment]; } - (BOOL)forceTextInputbarAdjustmentForResponder:(UIResponder *)responder { if ([responder isKindOfClass:[UIAlertController class]]) { return YES; } // On iOS 9, returning YES helps keeping the input view visible when the keyboard if presented from another app when using multi-tasking on iPad. return SLK_IS_IPAD; } - (void)didChangeKeyboardStatus:(SLKKeyboardStatus)status { // Notifies the view controller that the keyboard changed status. switch (status) { case SLKKeyboardStatusWillShow: return NSLog(@"Will Show"); case SLKKeyboardStatusDidShow: return NSLog(@"Did Show"); case SLKKeyboardStatusWillHide: return NSLog(@"Will Hide"); case SLKKeyboardStatusDidHide: return NSLog(@"Did Hide"); } } - (void)textWillUpdate { // Notifies the view controller that the text will update. [super textWillUpdate]; } - (void)textDidUpdate:(BOOL)animated { // Notifies the view controller that the text did update. [super textDidUpdate:animated]; } - (void)didPressLeftButton:(id)sender { // Notifies the view controller when the left button's action has been triggered, manually. [super didPressLeftButton:sender]; UIViewController *vc = [UIViewController new]; vc.view.backgroundColor = [UIColor whiteColor]; vc.title = @"Details"; [self.navigationController pushViewController:vc animated:YES]; } - (void)didPressRightButton:(id)sender { // Notifies the view controller when the right button's action has been triggered, manually or by using the keyboard return key. // This little trick validates any pending auto-correction or auto-spelling just after hitting the 'Send' button [self.textView refreshFirstResponder]; Message *message = [Message new]; message.username = [LoremIpsum name]; message.text = [self.textView.text copy]; NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; UITableViewRowAnimation rowAnimation = self.inverted ? UITableViewRowAnimationBottom : UITableViewRowAnimationTop; UITableViewScrollPosition scrollPosition = self.inverted ? UITableViewScrollPositionBottom : UITableViewScrollPositionTop; [self.tableView beginUpdates]; [self.messages insertObject:message atIndex:0]; [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:rowAnimation]; [self.tableView endUpdates]; [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:YES]; // Fixes the cell from blinking (because of the transform, when using translucent cells) // See https://github.com/slackhq/SlackTextViewController/issues/94#issuecomment-69929927 [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; [super didPressRightButton:sender]; } - (void)didPressArrowKey:(UIKeyCommand *)keyCommand { if ([keyCommand.input isEqualToString:UIKeyInputUpArrow] && self.textView.text.length == 0) { [self editLastMessage:nil]; } else { [super didPressArrowKey:keyCommand]; } } - (NSString *)keyForTextCaching { return [[NSBundle mainBundle] bundleIdentifier]; } - (void)didPasteMediaContent:(NSDictionary *)userInfo { // Notifies the view controller when the user has pasted a media (image, video, etc) inside of the text view. [super didPasteMediaContent:userInfo]; SLKPastableMediaType mediaType = [userInfo[SLKTextViewPastedItemMediaType] integerValue]; NSString *contentType = userInfo[SLKTextViewPastedItemContentType]; id data = userInfo[SLKTextViewPastedItemData]; NSLog(@"%s : %@ (type = %ld) | data : %@",__FUNCTION__, contentType, (unsigned long)mediaType, data); } - (void)willRequestUndo { // Notifies the view controller when a user did shake the device to undo the typed text [super willRequestUndo]; } - (void)didCommitTextEditing:(id)sender { // Notifies the view controller when tapped on the right "Accept" button for commiting the edited text self.editingMessage.text = [self.textView.text copy]; [self.tableView reloadData]; [super didCommitTextEditing:sender]; } - (void)didCancelTextEditing:(id)sender { // Notifies the view controller when tapped on the left "Cancel" button [super didCancelTextEditing:sender]; } - (BOOL)canPressRightButton { return [super canPressRightButton]; } - (BOOL)canShowTypingIndicator { #if DEBUG_CUSTOM_TYPING_INDICATOR return YES; #else return [super canShowTypingIndicator]; #endif } - (BOOL)shouldProcessTextForAutoCompletion { return [super shouldProcessTextForAutoCompletion]; } - (BOOL)shouldDisableTypingSuggestionForAutoCompletion { return [super shouldDisableTypingSuggestionForAutoCompletion]; } - (void)didChangeAutoCompletionPrefix:(NSString *)prefix andWord:(NSString *)word { NSArray *array = nil; self.searchResult = nil; if ([prefix isEqualToString:@"@"]) { if (word.length > 0) { array = [self.users filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self BEGINSWITH[c] %@", word]]; } else { array = self.users; } } else if ([prefix isEqualToString:@"#"] && word.length > 0) { array = [self.channels filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self BEGINSWITH[c] %@", word]]; } else if (([prefix isEqualToString:@":"] || [prefix isEqualToString:@"+:"]) && word.length > 1) { array = [self.emojis filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self BEGINSWITH[c] %@", word]]; } else if ([prefix isEqualToString:@"/"] && self.foundPrefixRange.location == 0) { if (word.length > 0) { array = [self.commands filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self BEGINSWITH[c] %@", word]]; } else { array = self.commands; } } if (array.count > 0) { array = [array sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; } self.searchResult = [[NSMutableArray alloc] initWithArray:array]; BOOL show = (self.searchResult.count > 0); [self showAutoCompletionView:show]; } - (CGFloat)heightForAutoCompletionView { CGFloat cellHeight = [self.autoCompletionView.delegate tableView:self.autoCompletionView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; return cellHeight*self.searchResult.count; } #pragma mark - SLKTextViewDelegate Methods - (BOOL)textView:(SLKTextView *)textView shouldOfferFormattingForSymbol:(NSString *)symbol { if ([symbol isEqualToString:@">"]) { NSRange selection = textView.selectedRange; // The Quote formatting only applies new paragraphs if (selection.location == 0 && selection.length > 0) { return YES; } // or older paragraphs too NSString *prevString = [textView.text substringWithRange:NSMakeRange(selection.location-1, 1)]; if ([[NSCharacterSet newlineCharacterSet] characterIsMember:[prevString characterAtIndex:0]]) { return YES; } return NO; } return [super textView:textView shouldOfferFormattingForSymbol:symbol]; } - (BOOL)textView:(SLKTextView *)textView shouldInsertSuffixForFormattingWithSymbol:(NSString *)symbol prefixRange:(NSRange)prefixRange { if ([symbol isEqualToString:@">"]) { return NO; } return [super textView:textView shouldInsertSuffixForFormattingWithSymbol:symbol prefixRange:prefixRange]; } #pragma mark - UITextViewDelegate Methods - (BOOL)textView:(SLKTextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { return [super textView:textView shouldChangeTextInRange:range replacementText:text]; } #pragma mark - UITableViewDataSource Methods - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if ([tableView isEqual:self.tableView]) { return self.messages.count; } else { return self.searchResult.count; } } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { if ([tableView isEqual:self.tableView]) { return [self messageCellForRowAtIndexPath:indexPath]; } else { return [self autoCompletionCellForRowAtIndexPath:indexPath]; } } - (MessageTableViewCell *)messageCellForRowAtIndexPath:(NSIndexPath *)indexPath { MessageTableViewCell *cell = (MessageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:MessengerCellIdentifier]; if (cell.gestureRecognizers.count == 0) { UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(didLongPressCell:)]; [cell addGestureRecognizer:longPress]; } Message *message = self.messages[indexPath.row]; cell.titleLabel.text = message.username; cell.bodyLabel.text = message.text; cell.indexPath = indexPath; cell.usedForMessage = YES; // Cells must inherit the table view's transform // This is very important, since the main table view may be inverted cell.transform = self.tableView.transform; return cell; } - (MessageTableViewCell *)autoCompletionCellForRowAtIndexPath:(NSIndexPath *)indexPath { MessageTableViewCell *cell = (MessageTableViewCell *)[self.autoCompletionView dequeueReusableCellWithIdentifier:AutoCompletionCellIdentifier]; cell.indexPath = indexPath; NSString *text = self.searchResult[indexPath.row]; if ([self.foundPrefix isEqualToString:@"#"]) { text = [NSString stringWithFormat:@"# %@", text]; } else if (([self.foundPrefix isEqualToString:@":"] || [self.foundPrefix isEqualToString:@"+:"])) { text = [NSString stringWithFormat:@":%@:", text]; } cell.titleLabel.text = text; cell.selectionStyle = UITableViewCellSelectionStyleDefault; return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if ([tableView isEqual:self.tableView]) { Message *message = self.messages[indexPath.row]; NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; paragraphStyle.alignment = NSTextAlignmentLeft; CGFloat pointSize = [MessageTableViewCell defaultFontSize]; NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:pointSize], NSParagraphStyleAttributeName: paragraphStyle}; CGFloat width = CGRectGetWidth(tableView.frame)-kMessageTableViewCellAvatarHeight; width -= 25.0; CGRect titleBounds = [message.username boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:NULL]; CGRect bodyBounds = [message.text boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:NULL]; if (message.text.length == 0) { return 0.0; } CGFloat height = CGRectGetHeight(titleBounds); height += CGRectGetHeight(bodyBounds); height += 40.0; if (height < kMessageTableViewCellMinimumHeight) { height = kMessageTableViewCellMinimumHeight; } return height; } else { return kMessageTableViewCellMinimumHeight; } } #pragma mark - UITableViewDelegate Methods - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if ([tableView isEqual:self.autoCompletionView]) { NSMutableString *item = [self.searchResult[indexPath.row] mutableCopy]; if ([self.foundPrefix isEqualToString:@"@"] && self.foundPrefixRange.location == 0) { [item appendString:@":"]; } else if (([self.foundPrefix isEqualToString:@":"] || [self.foundPrefix isEqualToString:@"+:"])) { [item appendString:@":"]; } [item appendString:@" "]; [self acceptAutoCompletionWithString:item keepPrefix:YES]; } } #pragma mark - UIScrollViewDelegate Methods - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // Since SLKTextViewController uses UIScrollViewDelegate to update a few things, it is important that if you override this method, to call super. [super scrollViewDidScroll:scrollView]; } #pragma mark - Lifeterm - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end ================================================ FILE: Examples/Messenger-Shared/TypingIndicatorView.h ================================================ // // TypingIndicatorView.h // Messenger // // Created by Ignacio Romero Z. on 6/27/15. // Copyright (c) 2015 Slack Technologies, Inc. All rights reserved. // #import #import "SLKTypingIndicatorProtocol.h" static CGFloat kTypingIndicatorViewMinimumHeight = 80.0; static CGFloat kTypingIndicatorViewAvatarHeight = 30.0; @interface TypingIndicatorView : UIView - (void)presentIndicatorWithName:(NSString *)name image:(UIImage *)image; - (void)dismissIndicator; @end ================================================ FILE: Examples/Messenger-Shared/TypingIndicatorView.m ================================================ // // TypingIndicatorView.m // Messenger // // Created by Ignacio Romero Z. on 6/27/15. // Copyright (c) 2015 Slack Technologies, Inc. All rights reserved. // #import "TypingIndicatorView.h" #import "SLKUIConstants.h" @interface TypingIndicatorView () @property (nonatomic, strong) UIImageView *thumbnailView; @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) CAGradientLayer *backgroundGradient; @end @implementation TypingIndicatorView @synthesize visible = _visible; - (instancetype)init { self = [super init]; if (self) { [self configureSubviews]; } return self; } - (void)configureSubviews { [self addSubview:self.thumbnailView]; [self addSubview:self.titleLabel]; [self.layer insertSublayer:self.backgroundGradient atIndex:0]; NSDictionary *views = @{@"thumbnailView": self.thumbnailView, @"titleLabel": self.titleLabel }; NSDictionary *metrics = @{@"invertedThumbSize": @(-kTypingIndicatorViewAvatarHeight/2.0), }; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-5-[thumbnailView]-10-[titleLabel]-(>=0)-|" options:0 metrics:metrics views:views]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=0)-[thumbnailView]-(invertedThumbSize)-|" options:0 metrics:metrics views:views]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=0)-[titleLabel]-(3@750)-|" options:0 metrics:metrics views:views]]; } #pragma mark - SLKTypingIndicatorProtocol - (void)dismissIndicator { if (self.isVisible) { self.visible = NO; } } #pragma mark - UIView - (void)layoutSubviews { [super layoutSubviews]; self.backgroundGradient.frame = self.bounds; } #pragma mark - Getters - (UIImageView *)thumbnailView { if (!_thumbnailView) { _thumbnailView = [UIImageView new]; _thumbnailView.translatesAutoresizingMaskIntoConstraints = NO; _thumbnailView.userInteractionEnabled = NO; _thumbnailView.backgroundColor = [UIColor grayColor]; _thumbnailView.contentMode = UIViewContentModeTopLeft; _thumbnailView.layer.cornerRadius = kTypingIndicatorViewAvatarHeight/2.0; _thumbnailView.layer.masksToBounds = YES; } return _thumbnailView; } - (UILabel *)titleLabel { if (!_titleLabel) { _titleLabel = [UILabel new]; _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; _titleLabel.backgroundColor = [UIColor clearColor]; _titleLabel.userInteractionEnabled = NO; _titleLabel.numberOfLines = 1; _titleLabel.contentMode = UIViewContentModeTopLeft; _titleLabel.font = [UIFont systemFontOfSize:12.0]; _titleLabel.textColor = [UIColor lightGrayColor]; } return _titleLabel; } - (CAGradientLayer *)backgroundGradient { if (!_backgroundGradient) { _backgroundGradient = [CAGradientLayer layer]; _backgroundGradient.frame = CGRectMake(0.0, 0.0, CGRectGetWidth(SLKKeyWindowBounds()), [self height]); _backgroundGradient.colors = @[(id)[UIColor colorWithWhite:1.0 alpha:0].CGColor, (id)[UIColor colorWithWhite:1.0 alpha:0.9].CGColor, (id)[UIColor colorWithWhite:1.0 alpha:1.0].CGColor]; _backgroundGradient.locations = @[@0, @0.5, @1]; } return _backgroundGradient; } - (CGSize)intrinsicContentSize { return CGSizeMake(UIViewNoIntrinsicMetric, [self height]); } - (CGFloat)height { CGFloat height = 13.0; height += self.titleLabel.font.lineHeight; return height; } #pragma mark - TypingIndicatorView - (void)presentIndicatorWithName:(NSString *)name image:(UIImage *)image { if (self.isVisible || name.length == 0 || !image) { return; } NSString *text = [NSString stringWithFormat:@"%@ is typing...", name]; NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text]; [attributedString addAttributes:@{NSFontAttributeName: [UIFont boldSystemFontOfSize:12.0]} range:[text rangeOfString:name]]; self.titleLabel.attributedText = attributedString; self.thumbnailView.image = image; self.visible = YES; } #pragma mark - Hit Testing - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; [self dismissIndicator]; } #pragma mark - Lifeterm - (void)dealloc { _titleLabel = nil; _thumbnailView = nil; _backgroundGradient = nil; } @end ================================================ FILE: Examples/Messenger-Storyboard/AppDelegate.h ================================================ // // AppDelegate.h // Messenger-Storyboard // // Created by Ignacio Romero Zurbuchen on 10/16/14. // Copyright (c) 2014 Slack Technologies, Inc. All rights reserved. // #import @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @end ================================================ FILE: Examples/Messenger-Storyboard/AppDelegate.m ================================================ // // AppDelegate.m // Messenger-Storyboard // // Created by Ignacio Romero Zurbuchen on 10/16/14. // Copyright (c) 2014 Slack Technologies, Inc. All rights reserved. // #import "AppDelegate.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window.backgroundColor = [UIColor whiteColor]; return YES; } @end ================================================ FILE: Examples/Messenger-Storyboard/Base.lproj/LaunchScreen.xib ================================================ ================================================ FILE: Examples/Messenger-Storyboard/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Examples/Messenger-Storyboard/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName Messenger CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 LSRequiresIPhoneOS NSAppTransportSecurity NSAppTransportSecurity UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance ================================================ FILE: Examples/Messenger-Storyboard/ViewController.h ================================================ // // ViewController.h // Messenger // // Created by Ignacio Romero Z. on 4/8/15. // Copyright (c) 2015 Slack Technologies, Inc. All rights reserved. // #import @interface ViewController : UIViewController - (IBAction)showMessages:(id)sender; @end ================================================ FILE: Examples/Messenger-Storyboard/ViewController.m ================================================ // // ViewController.m // Messenger // // Created by Ignacio Romero Z. on 4/8/15. // Copyright (c) 2015 Slack Technologies, Inc. All rights reserved. // #import "ViewController.h" @implementation ViewController - (IBAction)showMessages:(id)sender { [self performSegueWithIdentifier:@"show_messages" sender:sender]; } @end ================================================ FILE: Examples/Messenger-Storyboard/main.m ================================================ // // main.m // Messenger-Storyboard // // Created by Ignacio Romero Z. on 10/16/14. // Copyright (c) 2014 Slack Technologies, Inc. All rights reserved. // #import #import "AppDelegate.h" int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ================================================ FILE: Examples/Messenger-Swift/AppDelegate.swift ================================================ // // AppDelegate.swift // Messenger // // Created by Ignacio Romero Zurbuchen on 10/16/14. // Copyright (c) 2014 Slack Technologies, Inc. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { window?.backgroundColor = UIColor.white return true } } ================================================ FILE: Examples/Messenger-Swift/Base.lproj/LaunchScreen.xib ================================================ ================================================ FILE: Examples/Messenger-Swift/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Examples/Messenger-Swift/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName Messenger CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: Examples/Messenger-Swift/MessageViewController.swift ================================================ // // MessageViewController.swift // Messenger // // Created by Ignacio Romero Zurbuchen on 10/16/14. // Copyright (c) 2014 Slack Technologies, Inc. All rights reserved. // let DEBUG_CUSTOM_TYPING_INDICATOR = false class MessageViewController: SLKTextViewController { var messages = [Message]() var users: Array = ["Allen", "Anna", "Alicia", "Arnold", "Armando", "Antonio", "Brad", "Catalaya", "Christoph", "Emerson", "Eric", "Everyone", "Steve"] var channels: Array = ["General", "Random", "iOS", "Bugs", "Sports", "Android", "UI", "SSB"] var emojis: Array = ["-1", "m", "man", "machine", "block-a", "block-b", "bowtie", "boar", "boat", "book", "bookmark", "neckbeard", "metal", "fu", "feelsgood"] var commands: Array = ["msg", "call", "text", "skype", "kick", "invite"] var searchResult: [String]? var pipWindow: UIWindow? var editingMessage = Message() override var tableView: UITableView { get { return super.tableView! } } // MARK: - Initialisation override class func tableViewStyle(for decoder: NSCoder) -> UITableViewStyle { return .plain } func commonInit() { NotificationCenter.default.addObserver(self.tableView, selector: #selector(UITableView.reloadData), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(MessageViewController.textInputbarDidMove(_:)), name: NSNotification.Name.SLKTextInputbarDidMove, object: nil) } override func viewDidLoad() { // Register a SLKTextView subclass, if you need any special appearance and/or behavior customisation. self.registerClass(forTextView: MessageTextView.classForCoder()) if DEBUG_CUSTOM_TYPING_INDICATOR == true { // Register a UIView subclass, conforming to SLKTypingIndicatorProtocol, to use a custom typing indicator view. self.registerClass(forTypingIndicatorView: TypingIndicatorView.classForCoder()) } super.viewDidLoad() self.commonInit() // Example's configuration self.configureDataSource() self.configureActionItems() // SLKTVC's configuration self.bounces = true self.shakeToClearEnabled = true self.isKeyboardPanningEnabled = true self.shouldScrollToBottomAfterKeyboardShows = false self.isInverted = true self.leftButton.setImage(UIImage(named: "icn_upload"), for: UIControlState()) self.leftButton.tintColor = UIColor.gray self.rightButton.setTitle(NSLocalizedString("Send", comment: ""), for: UIControlState()) self.textInputbar.autoHideRightButton = true self.textInputbar.maxCharCount = 256 self.textInputbar.counterStyle = .split self.textInputbar.counterPosition = .top self.textInputbar.editorTitle.textColor = UIColor.darkGray self.textInputbar.editorLeftButton.tintColor = UIColor(red: 0/255, green: 122/255, blue: 255/255, alpha: 1) self.textInputbar.editorRightButton.tintColor = UIColor(red: 0/255, green: 122/255, blue: 255/255, alpha: 1) if DEBUG_CUSTOM_TYPING_INDICATOR == false { self.typingIndicatorView!.canResignByTouch = true } self.tableView.separatorStyle = .none self.tableView.register(MessageTableViewCell.classForCoder(), forCellReuseIdentifier: MessengerCellIdentifier) self.autoCompletionView.register(MessageTableViewCell.classForCoder(), forCellReuseIdentifier: AutoCompletionCellIdentifier) self.registerPrefixes(forAutoCompletion: ["@", "#", ":", "+:", "/"]) self.textView.placeholder = "Message"; self.textView.registerMarkdownFormattingSymbol("*", withTitle: "Bold") self.textView.registerMarkdownFormattingSymbol("_", withTitle: "Italics") self.textView.registerMarkdownFormattingSymbol("~", withTitle: "Strike") self.textView.registerMarkdownFormattingSymbol("`", withTitle: "Code") self.textView.registerMarkdownFormattingSymbol("```", withTitle: "Preformatted") self.textView.registerMarkdownFormattingSymbol(">", withTitle: "Quote") } // MARK: - Lifeterm override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } deinit { NotificationCenter.default.removeObserver(self) } } extension MessageViewController { // MARK: - Example's Configuration func configureDataSource() { var array = [Message]() for _ in 0..<100 { let words = Int((arc4random() % 40)+1) let message = Message() message.username = LoremIpsum.name() message.text = LoremIpsum.words(withNumber: words) array.append(message) } let reversed = array.reversed() self.messages.append(contentsOf: reversed) } func configureActionItems() { let arrowItem = UIBarButtonItem(image: UIImage(named: "icn_arrow_down"), style: .plain, target: self, action: #selector(MessageViewController.hideOrShowTextInputbar(_:))) let editItem = UIBarButtonItem(image: UIImage(named: "icn_editing"), style: .plain, target: self, action: #selector(MessageViewController.editRandomMessage(_:))) let typeItem = UIBarButtonItem(image: UIImage(named: "icn_typing"), style: .plain, target: self, action: #selector(MessageViewController.simulateUserTyping(_:))) let appendItem = UIBarButtonItem(image: UIImage(named: "icn_append"), style: .plain, target: self, action: #selector(MessageViewController.fillWithText(_:))) let pipItem = UIBarButtonItem(image: UIImage(named: "icn_pic"), style: .plain, target: self, action: #selector(MessageViewController.togglePIPWindow(_:))) self.navigationItem.rightBarButtonItems = [arrowItem, pipItem, editItem, appendItem, typeItem] } // MARK: - Action Methods func hideOrShowTextInputbar(_ sender: AnyObject) { guard let buttonItem = sender as? UIBarButtonItem else { return } let hide = !self.isTextInputbarHidden let image = hide ? UIImage(named: "icn_arrow_up") : UIImage(named: "icn_arrow_down") self.setTextInputbarHidden(hide, animated: true) buttonItem.image = image } func fillWithText(_ sender: AnyObject) { if self.textView.text.characters.count == 0 { var sentences = Int(arc4random() % 4) if sentences <= 1 { sentences = 1 } self.textView.text = LoremIpsum.sentences(withNumber: sentences) } else { self.textView.slk_insertText(atCaretRange: " " + LoremIpsum.word()) } } func simulateUserTyping(_ sender: AnyObject) { if !self.canShowTypingIndicator() { return } if DEBUG_CUSTOM_TYPING_INDICATOR == true { guard let view = self.typingIndicatorProxyView as? TypingIndicatorView else { return } let scale = UIScreen.main.scale let imgSize = CGSize(width: kTypingIndicatorViewAvatarHeight*scale, height: kTypingIndicatorViewAvatarHeight*scale) // This will cause the typing indicator to show after a delay ¯\_(ツ)_/¯ LoremIpsum.asyncPlaceholderImage(with: imgSize, completion: { (image) -> Void in guard let cgImage = image?.cgImage else { return } let thumbnail = UIImage(cgImage: cgImage, scale: scale, orientation: .up) view.presentIndicator(withName: LoremIpsum.name(), image: thumbnail) }) } else { self.typingIndicatorView!.insertUsername(LoremIpsum.name()) } } func didLongPressCell(_ gesture: UIGestureRecognizer) { guard let view = gesture.view else { return } if gesture.state != .began { return } if #available(iOS 8, *) { let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) alertController.modalPresentationStyle = .popover alertController.popoverPresentationController?.sourceView = view.superview alertController.popoverPresentationController?.sourceRect = view.frame alertController.addAction(UIAlertAction(title: "Edit Message", style: .default, handler: { [unowned self] (action) -> Void in self.editCellMessage(gesture) })) alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) self.navigationController?.present(alertController, animated: true, completion: nil) } else { self.editCellMessage(gesture) } } func editCellMessage(_ gesture: UIGestureRecognizer) { guard let cell = gesture.view as? MessageTableViewCell else { return } self.editingMessage = self.messages[cell.indexPath.row] self.editText(self.editingMessage.text) self.tableView.scrollToRow(at: cell.indexPath, at: .bottom, animated: true) } func editRandomMessage(_ sender: AnyObject) { var sentences = Int(arc4random() % 10) if sentences <= 1 { sentences = 1 } self.editText(LoremIpsum.sentences(withNumber: sentences)) } func editLastMessage(_ sender: AnyObject?) { if self.textView.text.characters.count > 0 { return } let lastSectionIndex = self.tableView.numberOfSections-1 let lastRowIndex = self.tableView.numberOfRows(inSection: lastSectionIndex)-1 let lastMessage = self.messages[lastRowIndex] self.editText(lastMessage.text) self.tableView.scrollToRow(at: IndexPath(row: lastRowIndex, section: lastSectionIndex), at: .bottom, animated: true) } func togglePIPWindow(_ sender: AnyObject) { if self.pipWindow == nil { self.showPIPWindow(sender) } else { self.hidePIPWindow(sender) } } func showPIPWindow(_ sender: AnyObject) { var frame = CGRect(x: self.view.frame.width - 60.0, y: 0.0, width: 50.0, height: 50.0) frame.origin.y = self.textInputbar.frame.minY - 60.0 self.pipWindow = UIWindow(frame: frame) self.pipWindow?.backgroundColor = UIColor.black self.pipWindow?.layer.cornerRadius = 10 self.pipWindow?.layer.masksToBounds = true self.pipWindow?.isHidden = false self.pipWindow?.alpha = 0.0 UIApplication.shared.keyWindow?.addSubview(self.pipWindow!) UIView.animate(withDuration: 0.25, animations: { [unowned self] () -> Void in self.pipWindow?.alpha = 1.0 }) } func hidePIPWindow(_ sender: AnyObject) { UIView.animate(withDuration: 0.3, animations: { [unowned self] () -> Void in self.pipWindow?.alpha = 0.0 }, completion: { [unowned self] (finished) -> Void in self.pipWindow?.isHidden = true self.pipWindow = nil }) } func textInputbarDidMove(_ note: Notification) { guard let pipWindow = self.pipWindow else { return } guard let userInfo = (note as NSNotification).userInfo else { return } guard let value = userInfo["origin"] as? NSValue else { return } var frame = pipWindow.frame frame.origin.y = value.cgPointValue.y - 60.0 pipWindow.frame = frame } } extension MessageViewController { // MARK: - Overriden Methods override func ignoreTextInputbarAdjustment() -> Bool { return super.ignoreTextInputbarAdjustment() } override func forceTextInputbarAdjustment(for responder: UIResponder!) -> Bool { if #available(iOS 8.0, *) { guard let _ = responder as? UIAlertController else { // On iOS 9, returning YES helps keeping the input view visible when the keyboard if presented from another app when using multi-tasking on iPad. return UIDevice.current.userInterfaceIdiom == .pad } return true } else { return UIDevice.current.userInterfaceIdiom == .pad } } // Notifies the view controller that the keyboard changed status. override func didChangeKeyboardStatus(_ status: SLKKeyboardStatus) { switch status { case .willShow: print("Will Show") case .didShow: print("Did Show") case .willHide: print("Will Hide") case .didHide: print("Did Hide") } } // Notifies the view controller that the text will update. override func textWillUpdate() { super.textWillUpdate() } // Notifies the view controller that the text did update. override func textDidUpdate(_ animated: Bool) { super.textDidUpdate(animated) } // Notifies the view controller when the left button's action has been triggered, manually. override func didPressLeftButton(_ sender: Any!) { super.didPressLeftButton(sender) self.dismissKeyboard(true) self.performSegue(withIdentifier: "Push", sender: nil) } // Notifies the view controller when the right button's action has been triggered, manually or by using the keyboard return key. override func didPressRightButton(_ sender: Any!) { // This little trick validates any pending auto-correction or auto-spelling just after hitting the 'Send' button self.textView.refreshFirstResponder() let message = Message() message.username = LoremIpsum.name() message.text = self.textView.text let indexPath = IndexPath(row: 0, section: 0) let rowAnimation: UITableViewRowAnimation = self.isInverted ? .bottom : .top let scrollPosition: UITableViewScrollPosition = self.isInverted ? .bottom : .top self.tableView.beginUpdates() self.messages.insert(message, at: 0) self.tableView.insertRows(at: [indexPath], with: rowAnimation) self.tableView.endUpdates() self.tableView.scrollToRow(at: indexPath, at: scrollPosition, animated: true) // Fixes the cell from blinking (because of the transform, when using translucent cells) // See https://github.com/slackhq/SlackTextViewController/issues/94#issuecomment-69929927 self.tableView.reloadRows(at: [indexPath], with: .automatic) super.didPressRightButton(sender) } override func didPressArrowKey(_ keyCommand: UIKeyCommand?) { guard let keyCommand = keyCommand else { return } if keyCommand.input == UIKeyInputUpArrow && self.textView.text.characters.count == 0 { self.editLastMessage(nil) } else { super.didPressArrowKey(keyCommand) } } override func keyForTextCaching() -> String? { return Bundle.main.bundleIdentifier } // Notifies the view controller when the user has pasted a media (image, video, etc) inside of the text view. override func didPasteMediaContent(_ userInfo: [AnyHashable: Any]) { super.didPasteMediaContent(userInfo) let mediaType = (userInfo[SLKTextViewPastedItemMediaType] as? NSNumber)?.intValue let contentType = userInfo[SLKTextViewPastedItemContentType] let data = userInfo[SLKTextViewPastedItemData] print("didPasteMediaContent : \(contentType) (type = \(mediaType) | data : \(data))") } // Notifies the view controller when a user did shake the device to undo the typed text override func willRequestUndo() { super.willRequestUndo() } // Notifies the view controller when tapped on the right "Accept" button for commiting the edited text override func didCommitTextEditing(_ sender: Any) { self.editingMessage.text = self.textView.text self.tableView.reloadData() super.didCommitTextEditing(sender) } // Notifies the view controller when tapped on the left "Cancel" button override func didCancelTextEditing(_ sender: Any) { super.didCancelTextEditing(sender) } override func canPressRightButton() -> Bool { return super.canPressRightButton() } override func canShowTypingIndicator() -> Bool { if DEBUG_CUSTOM_TYPING_INDICATOR == true { return true } else { return super.canShowTypingIndicator() } } override func shouldProcessTextForAutoCompletion() -> Bool { return true } override func didChangeAutoCompletionPrefix(_ prefix: String, andWord word: String) { var array:Array = [] let wordPredicate = NSPredicate(format: "self BEGINSWITH[c] %@", word); self.searchResult = nil if prefix == "@" { if word.characters.count > 0 { array = self.users.filter { wordPredicate.evaluate(with: $0) }; } else { array = self.users } } else if prefix == "#" { if word.characters.count > 0 { array = self.channels.filter { wordPredicate.evaluate(with: $0) }; } else { array = self.channels } } else if (prefix == ":" || prefix == "+:") && word.characters.count > 0 { array = self.emojis.filter { wordPredicate.evaluate(with: $0) }; } else if prefix == "/" && self.foundPrefixRange.location == 0 { if word.characters.count > 0 { array = self.commands.filter { wordPredicate.evaluate(with: $0) }; } else { array = self.commands } } var show = false if array.count > 0 { let sortedArray = array.sorted { $0.localizedCaseInsensitiveCompare($1) == ComparisonResult.orderedAscending } self.searchResult = sortedArray show = sortedArray.count > 0 } self.showAutoCompletionView(show) } override func heightForAutoCompletionView() -> CGFloat { guard let searchResult = self.searchResult else { return 0 } let cellHeight = self.autoCompletionView.delegate?.tableView!(self.autoCompletionView, heightForRowAt: IndexPath(row: 0, section: 0)) guard let height = cellHeight else { return 0 } return height * CGFloat(searchResult.count) } } extension MessageViewController { // MARK: - UITableViewDataSource Methods override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if tableView == self.tableView { return self.messages.count } else { if let searchResult = self.searchResult { return searchResult.count } } return 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if tableView == self.tableView { return self.messageCellForRowAtIndexPath(indexPath) } else { return self.autoCompletionCellForRowAtIndexPath(indexPath) } } func messageCellForRowAtIndexPath(_ indexPath: IndexPath) -> MessageTableViewCell { let cell = self.tableView.dequeueReusableCell(withIdentifier: MessengerCellIdentifier) as! MessageTableViewCell if cell.gestureRecognizers?.count == nil { let longPress = UILongPressGestureRecognizer(target: self, action: #selector(MessageViewController.didLongPressCell(_:))) cell.addGestureRecognizer(longPress) } let message = self.messages[(indexPath as NSIndexPath).row] cell.titleLabel.text = message.username cell.bodyLabel.text = message.text cell.indexPath = indexPath cell.usedForMessage = true // Cells must inherit the table view's transform // This is very important, since the main table view may be inverted cell.transform = self.tableView.transform return cell } func autoCompletionCellForRowAtIndexPath(_ indexPath: IndexPath) -> MessageTableViewCell { let cell = self.autoCompletionView.dequeueReusableCell(withIdentifier: AutoCompletionCellIdentifier) as! MessageTableViewCell cell.indexPath = indexPath cell.selectionStyle = .default guard let searchResult = self.searchResult else { return cell } guard let prefix = self.foundPrefix else { return cell } var text = searchResult[(indexPath as NSIndexPath).row] if prefix == "#" { text = "# " + text } else if prefix == ":" || prefix == "+:" { text = ":\(text):" } cell.titleLabel.text = text return cell } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { if tableView == self.tableView { let message = self.messages[(indexPath as NSIndexPath).row] let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineBreakMode = .byWordWrapping paragraphStyle.alignment = .left let pointSize = MessageTableViewCell.defaultFontSize() let attributes = [ NSFontAttributeName : UIFont.systemFont(ofSize: pointSize), NSParagraphStyleAttributeName : paragraphStyle ] var width = tableView.frame.width-kMessageTableViewCellAvatarHeight width -= 25.0 let titleBounds = (message.username as NSString).boundingRect(with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: attributes, context: nil) let bodyBounds = (message.text as NSString).boundingRect(with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: attributes, context: nil) if message.text.characters.count == 0 { return 0 } var height = titleBounds.height height += bodyBounds.height height += 40 if height < kMessageTableViewCellMinimumHeight { height = kMessageTableViewCellMinimumHeight } return height } else { return kMessageTableViewCellMinimumHeight } } // MARK: - UITableViewDelegate Methods override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if tableView == self.autoCompletionView { guard let searchResult = self.searchResult else { return } var item = searchResult[(indexPath as NSIndexPath).row] if self.foundPrefix == "@" && self.foundPrefixRange.location == 0 { item += ":" } else if self.foundPrefix == ":" || self.foundPrefix == "+:" { item += ":" } item += " " self.acceptAutoCompletion(with: item, keepPrefix: true) } } } extension MessageViewController { // MARK: - UIScrollViewDelegate Methods // Since SLKTextViewController uses UIScrollViewDelegate to update a few things, it is important that if you override this method, to call super. override func scrollViewDidScroll(_ scrollView: UIScrollView) { super.scrollViewDidScroll(scrollView) } } extension MessageViewController { // MARK: - UITextViewDelegate Methods override func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { return true } override func textViewShouldEndEditing(_ textView: UITextView) -> Bool { // Since SLKTextViewController uses UIScrollViewDelegate to update a few things, it is important that if you override this method, to call super. return true } override func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { return super.textView(textView, shouldChangeTextIn: range, replacementText: text) } override func textView(_ textView: SLKTextView, shouldOfferFormattingForSymbol symbol: String) -> Bool { if symbol == ">" { let selection = textView.selectedRange // The Quote formatting only applies new paragraphs if selection.location == 0 && selection.length > 0 { return true } // or older paragraphs too let prevString = (textView.text as NSString).substring(with: NSMakeRange(selection.location-1, 1)) if CharacterSet.newlines.contains(UnicodeScalar((prevString as NSString).character(at: 0))!) { return true } return false } return super.textView(textView, shouldOfferFormattingForSymbol: symbol) } override func textView(_ textView: SLKTextView, shouldInsertSuffixForFormattingWithSymbol symbol: String, prefixRange: NSRange) -> Bool { if symbol == ">" { return false } return super.textView(textView, shouldInsertSuffixForFormattingWithSymbol: symbol, prefixRange: prefixRange) } } ================================================ FILE: Examples/Messenger-iPad-Sheet/AppDelegate.h ================================================ // // AppDelegate.h // Messenger-Programatic-iPad-Sheet // // Created by Bob Spryn on 1/23/15. // Copyright (c) 2015 Slack Technologies, Inc. All rights reserved. // #import @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @end ================================================ FILE: Examples/Messenger-iPad-Sheet/AppDelegate.m ================================================ // // AppDelegate.m // Messenger-Programatic-iPad-Sheet // // Created by Bob Spryn on 1/23/15. // Copyright (c) 2015 Slack Technologies, Inc. All rights reserved. // #import "AppDelegate.h" #import "SLKTextViewController.h" #import "MessageViewController.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; MessageViewController *messageVC = [MessageViewController new]; UINavigationController *navVC = [[UINavigationController alloc] initWithRootViewController:messageVC]; navVC.modalPresentationStyle = UIModalPresentationFormSheet; navVC.modalTransitionStyle = UIModalTransitionStyleCoverVertical; self.window.rootViewController = [UIViewController new]; [self.window makeKeyAndVisible]; [self.window.rootViewController presentViewController:navVC animated:YES completion:^{ [messageVC presentKeyboard:YES]; }]; return YES; } @end ================================================ FILE: Examples/Messenger-iPad-Sheet/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName Messenger CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: Examples/Messenger-iPad-Sheet/main.m ================================================ // // main.m // Messenger-Programatic-iPad-Sheet // // Created by Bob Spryn on 1/23/15. // Copyright (c) 2015 Slack Technologies, Inc. All rights reserved. // #import #import "AppDelegate.h" int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ================================================ FILE: Examples/Messenger.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 35A4C7C11DE3FD17F022DC0C /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DDFA9095EABC480682E6396C /* libPods.a */; }; 49FEFA70DA01AB47666A8D63 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DDFA9095EABC480682E6396C /* libPods.a */; }; 4A8EFBBFAC0F49199AAABF46 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DDFA9095EABC480682E6396C /* libPods.a */; }; 4F3256191AA77386008C1DD9 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F3256161AA77386008C1DD9 /* AppDelegate.m */; }; 4F32561B1AA77386008C1DD9 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F3256181AA77386008C1DD9 /* main.m */; }; 4F32561C1AA77427008C1DD9 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F86BF6C19F011D0007A3D4A /* AppDelegate.m */; }; 4F32561D1AA77430008C1DD9 /* MessageTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F7C6BA41A6E208E006E3FAB /* MessageTextView.m */; }; 4F32561E1AA77431008C1DD9 /* MessageTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F7C6BA41A6E208E006E3FAB /* MessageTextView.m */; }; 4F3EDB4D199ED00F004C15D6 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F3EDB4C199ED00F004C15D6 /* Foundation.framework */; }; 4F3EDB4F199ED00F004C15D6 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F3EDB4E199ED00F004C15D6 /* CoreGraphics.framework */; }; 4F3EDB51199ED00F004C15D6 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F3EDB50199ED00F004C15D6 /* UIKit.framework */; }; 4F3EDB59199ED00F004C15D6 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F3EDB58199ED00F004C15D6 /* main.m */; }; 4F3EDB5D199ED00F004C15D6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F3EDB5C199ED00F004C15D6 /* AppDelegate.m */; }; 4F86BF6A19F011D0007A3D4A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F86BF6919F011D0007A3D4A /* main.m */; }; 4F86BF7319F011D0007A3D4A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4F86BF7119F011D0007A3D4A /* Main.storyboard */; }; 4F86BF7819F011D0007A3D4A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4F86BF7619F011D0007A3D4A /* LaunchScreen.xib */; }; 4F86BF9319F01234007A3D4A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F86BF8E19F01234007A3D4A /* Images.xcassets */; }; 4F86BF9419F01234007A3D4A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F86BF8E19F01234007A3D4A /* Images.xcassets */; }; 4F86BF9519F01234007A3D4A /* MessageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F86BF9019F01234007A3D4A /* MessageTableViewCell.m */; }; 4F86BF9619F01234007A3D4A /* MessageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F86BF9019F01234007A3D4A /* MessageTableViewCell.m */; }; 4F86BF9719F01234007A3D4A /* MessageViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F86BF9219F01234007A3D4A /* MessageViewController.m */; }; 4F86BF9819F01234007A3D4A /* MessageViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F86BF9219F01234007A3D4A /* MessageViewController.m */; }; 4F86BFC819F050AF007A3D4A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F86BFC719F050AF007A3D4A /* AppDelegate.swift */; }; 4F86BFCA19F050AF007A3D4A /* (null) in Sources */ = {isa = PBXBuildFile; }; 4F86BFCD19F050AF007A3D4A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4F86BFCB19F050AF007A3D4A /* Main.storyboard */; }; 4F86BFD219F050AF007A3D4A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4F86BFD019F050AF007A3D4A /* LaunchScreen.xib */; }; 4F86BFE519F050DD007A3D4A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F86BF8E19F01234007A3D4A /* Images.xcassets */; }; 4F8ADA771A68C37400023752 /* Message.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F8ADA761A68C37400023752 /* Message.m */; }; 4F8ADA781A68C37400023752 /* Message.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F8ADA761A68C37400023752 /* Message.m */; }; 4F8ADA791A68C37400023752 /* Message.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F8ADA761A68C37400023752 /* Message.m */; }; 4F9DFA7F1AD4EDB900841D98 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F9DFA7D1AD4EDB100841D98 /* ViewController.m */; }; 4FF04E591B3F7A04004C3BED /* TypingIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FF04E581B3F7A04004C3BED /* TypingIndicatorView.m */; }; 71BE10281A72F3E50083EE32 /* MessageViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F86BF9219F01234007A3D4A /* MessageViewController.m */; }; 71BE10291A72F3E50083EE32 /* MessageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F86BF9019F01234007A3D4A /* MessageTableViewCell.m */; }; 71BE102B1A72F3E50083EE32 /* Message.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F8ADA761A68C37400023752 /* Message.m */; }; 71BE102C1A72F4180083EE32 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F3EDB4E199ED00F004C15D6 /* CoreGraphics.framework */; }; 71BE102D1A72F41E0083EE32 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F3EDB50199ED00F004C15D6 /* UIKit.framework */; }; 71BE102E1A72F4250083EE32 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F3EDB4C199ED00F004C15D6 /* Foundation.framework */; }; 71BE102F1A72F42F0083EE32 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F86BF8E19F01234007A3D4A /* Images.xcassets */; }; 71BE10301A76C4890083EE32 /* MessageTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F7C6BA41A6E208E006E3FAB /* MessageTextView.m */; }; 8C6D809F3C95C912F87F0D17 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DDFA9095EABC480682E6396C /* libPods.a */; }; 9F4466C31C9A1B6B0001609A /* MessageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F86BF9019F01234007A3D4A /* MessageTableViewCell.m */; }; 9F4466C51C9A1B6B0001609A /* MessageTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F7C6BA41A6E208E006E3FAB /* MessageTextView.m */; }; 9F4466C71C9A1B6B0001609A /* TypingIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FF04E581B3F7A04004C3BED /* TypingIndicatorView.m */; }; F556AFAE1BA7B41F009898BD /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F556AFAD1BA7B41F009898BD /* QuartzCore.framework */; }; F5CD59211C9FE0EC00DD9A4E /* MessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5A1CC971C542EB600908ECB /* MessageViewController.swift */; }; F5DE01401B9679C4005E9082 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4F86BF7619F011D0007A3D4A /* LaunchScreen.xib */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4F1AD51E19E727750027043E /* ___FILEBASENAME___.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "___FILEBASENAME___.h"; sourceTree = ""; }; 4F1AD51F19E727750027043E /* ___FILEBASENAME___.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "___FILEBASENAME___.m"; sourceTree = ""; }; 4F1AD52119E727750027043E /* ___FILEBASENAME___.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "___FILEBASENAME___.h"; sourceTree = ""; }; 4F1AD52219E727750027043E /* ___FILEBASENAME___.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "___FILEBASENAME___.m"; sourceTree = ""; }; 4F3256151AA77386008C1DD9 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 4F3256161AA77386008C1DD9 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 4F3256171AA77386008C1DD9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4F3256181AA77386008C1DD9 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4F3EDB49199ED00F004C15D6 /* Messenger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Messenger.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4F3EDB4C199ED00F004C15D6 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 4F3EDB4E199ED00F004C15D6 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 4F3EDB50199ED00F004C15D6 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 4F3EDB54199ED00F004C15D6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4F3EDB58199ED00F004C15D6 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4F3EDB5B199ED00F004C15D6 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 4F3EDB5C199ED00F004C15D6 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 4F7C6BA31A6E208E006E3FAB /* MessageTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MessageTextView.h; sourceTree = ""; }; 4F7C6BA41A6E208E006E3FAB /* MessageTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MessageTextView.m; sourceTree = ""; }; 4F86BF6519F011D0007A3D4A /* Messenger-Storyboard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Messenger-Storyboard.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 4F86BF6819F011D0007A3D4A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4F86BF6919F011D0007A3D4A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4F86BF6B19F011D0007A3D4A /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 4F86BF6C19F011D0007A3D4A /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 4F86BF7219F011D0007A3D4A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 4F86BF7719F011D0007A3D4A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 4F86BF8E19F01234007A3D4A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 4F86BF8F19F01234007A3D4A /* MessageTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MessageTableViewCell.h; sourceTree = ""; }; 4F86BF9019F01234007A3D4A /* MessageTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MessageTableViewCell.m; sourceTree = ""; }; 4F86BF9119F01234007A3D4A /* MessageViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MessageViewController.h; sourceTree = ""; }; 4F86BF9219F01234007A3D4A /* MessageViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MessageViewController.m; sourceTree = ""; }; 4F86BFC319F050AF007A3D4A /* Messenger-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Messenger-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 4F86BFC619F050AF007A3D4A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4F86BFC719F050AF007A3D4A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 4F86BFCC19F050AF007A3D4A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 4F86BFD119F050AF007A3D4A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 4F86BFE719F05519007A3D4A /* Bridge-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bridge-Header.h"; sourceTree = ""; }; 4F8ADA751A68C37400023752 /* Message.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Message.h; sourceTree = ""; }; 4F8ADA761A68C37400023752 /* Message.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Message.m; sourceTree = ""; }; 4F9DFA7C1AD4EDB100841D98 /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 4F9DFA7D1AD4EDB100841D98 /* ViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 4FF04E571B3F7A04004C3BED /* TypingIndicatorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TypingIndicatorView.h; sourceTree = ""; }; 4FF04E581B3F7A04004C3BED /* TypingIndicatorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TypingIndicatorView.m; sourceTree = ""; }; 622BF1F6C9CF013275EF6275 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; 71BE10021A72F3130083EE32 /* Messenger-iPad-Sheet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Messenger-iPad-Sheet.app"; sourceTree = BUILT_PRODUCTS_DIR; }; BA45477DD68F2909374B51A4 /* libPods-Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; C905E5DB18A83F74BD092CDD /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; DDFA9095EABC480682E6396C /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; E76FE7634642D0C745F673A5 /* libPods-Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F556AFAD1BA7B41F009898BD /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; F5A1CC971C542EB600908ECB /* MessageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 4F3EDB46199ED00F004C15D6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F556AFAE1BA7B41F009898BD /* QuartzCore.framework in Frameworks */, 4F3EDB4F199ED00F004C15D6 /* CoreGraphics.framework in Frameworks */, 4F3EDB51199ED00F004C15D6 /* UIKit.framework in Frameworks */, 4F3EDB4D199ED00F004C15D6 /* Foundation.framework in Frameworks */, 4A8EFBBFAC0F49199AAABF46 /* libPods.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 4F86BF6219F011D0007A3D4A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 8C6D809F3C95C912F87F0D17 /* libPods.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 4F86BFC019F050AF007A3D4A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 35A4C7C11DE3FD17F022DC0C /* libPods.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 71BE0FFF1A72F3130083EE32 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 71BE102E1A72F4250083EE32 /* Foundation.framework in Frameworks */, 71BE102D1A72F41E0083EE32 /* UIKit.framework in Frameworks */, 71BE102C1A72F4180083EE32 /* CoreGraphics.framework in Frameworks */, 49FEFA70DA01AB47666A8D63 /* libPods.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 4F1AD51C19E727640027043E /* Templates */ = { isa = PBXGroup; children = ( 4F1AD51D19E727750027043E /* UICollectionView */, 4F1AD52019E727750027043E /* UITableView */, ); name = Templates; sourceTree = ""; }; 4F1AD51D19E727750027043E /* UICollectionView */ = { isa = PBXGroup; children = ( 4F1AD51E19E727750027043E /* ___FILEBASENAME___.h */, 4F1AD51F19E727750027043E /* ___FILEBASENAME___.m */, ); name = UICollectionView; path = "../../File Templates/SlackTextView Controller.xctemplate/UICollectionView"; sourceTree = ""; }; 4F1AD52019E727750027043E /* UITableView */ = { isa = PBXGroup; children = ( 4F1AD52119E727750027043E /* ___FILEBASENAME___.h */, 4F1AD52219E727750027043E /* ___FILEBASENAME___.m */, ); name = UITableView; path = "../../File Templates/SlackTextView Controller.xctemplate/UITableView"; sourceTree = ""; }; 4F3256141AA77386008C1DD9 /* iPad Sheet Example */ = { isa = PBXGroup; children = ( 4F3256151AA77386008C1DD9 /* AppDelegate.h */, 4F3256161AA77386008C1DD9 /* AppDelegate.m */, 4F3256171AA77386008C1DD9 /* Info.plist */, 4F3256181AA77386008C1DD9 /* main.m */, ); name = "iPad Sheet Example"; path = "Messenger-iPad-Sheet"; sourceTree = ""; }; 4F3EDB40199ED00F004C15D6 = { isa = PBXGroup; children = ( 4F86BF8B19F011E9007A3D4A /* Examples */, 4F1AD51C19E727640027043E /* Templates */, 4F3EDB4B199ED00F004C15D6 /* Frameworks */, 4F3EDB4A199ED00F004C15D6 /* Products */, 68B0EE14916D384C7E703B9B /* Pods */, ); sourceTree = ""; }; 4F3EDB4A199ED00F004C15D6 /* Products */ = { isa = PBXGroup; children = ( 4F3EDB49199ED00F004C15D6 /* Messenger.app */, 4F86BF6519F011D0007A3D4A /* Messenger-Storyboard.app */, 4F86BFC319F050AF007A3D4A /* Messenger-Swift.app */, 71BE10021A72F3130083EE32 /* Messenger-iPad-Sheet.app */, ); name = Products; sourceTree = ""; }; 4F3EDB4B199ED00F004C15D6 /* Frameworks */ = { isa = PBXGroup; children = ( F556AFAD1BA7B41F009898BD /* QuartzCore.framework */, 4F3EDB4C199ED00F004C15D6 /* Foundation.framework */, 4F3EDB4E199ED00F004C15D6 /* CoreGraphics.framework */, 4F3EDB50199ED00F004C15D6 /* UIKit.framework */, DDFA9095EABC480682E6396C /* libPods.a */, BA45477DD68F2909374B51A4 /* libPods-Tests.a */, E76FE7634642D0C745F673A5 /* libPods-Tests.a */, ); name = Frameworks; sourceTree = ""; }; 4F3EDB52199ED00F004C15D6 /* Programatic Example */ = { isa = PBXGroup; children = ( 4F3EDB5B199ED00F004C15D6 /* AppDelegate.h */, 4F3EDB5C199ED00F004C15D6 /* AppDelegate.m */, 4F3EDB53199ED00F004C15D6 /* Supporting Files */, ); name = "Programatic Example"; path = "Messenger-Programatic"; sourceTree = ""; }; 4F3EDB53199ED00F004C15D6 /* Supporting Files */ = { isa = PBXGroup; children = ( 4F3EDB54199ED00F004C15D6 /* Info.plist */, 4F3EDB58199ED00F004C15D6 /* main.m */, ); name = "Supporting Files"; sourceTree = ""; }; 4F86BF6619F011D0007A3D4A /* Storyboard Example */ = { isa = PBXGroup; children = ( 4F86BF6B19F011D0007A3D4A /* AppDelegate.h */, 4F86BF6C19F011D0007A3D4A /* AppDelegate.m */, 4F9DFA7C1AD4EDB100841D98 /* ViewController.h */, 4F9DFA7D1AD4EDB100841D98 /* ViewController.m */, 4F86BF7119F011D0007A3D4A /* Main.storyboard */, 4F86BF7619F011D0007A3D4A /* LaunchScreen.xib */, 4F86BF6719F011D0007A3D4A /* Supporting Files */, ); name = "Storyboard Example"; path = "Messenger-Storyboard"; sourceTree = ""; }; 4F86BF6719F011D0007A3D4A /* Supporting Files */ = { isa = PBXGroup; children = ( 4F86BF6819F011D0007A3D4A /* Info.plist */, 4F86BF6919F011D0007A3D4A /* main.m */, ); name = "Supporting Files"; sourceTree = ""; }; 4F86BF8B19F011E9007A3D4A /* Examples */ = { isa = PBXGroup; children = ( 4F86BF8D19F01234007A3D4A /* Shared */, 4F3EDB52199ED00F004C15D6 /* Programatic Example */, 4F86BF6619F011D0007A3D4A /* Storyboard Example */, 4F3256141AA77386008C1DD9 /* iPad Sheet Example */, 4F86BFC419F050AF007A3D4A /* Swift Example */, ); name = Examples; sourceTree = ""; }; 4F86BF8D19F01234007A3D4A /* Shared */ = { isa = PBXGroup; children = ( 4F86BF9119F01234007A3D4A /* MessageViewController.h */, 4F86BF9219F01234007A3D4A /* MessageViewController.m */, 4F86BF8F19F01234007A3D4A /* MessageTableViewCell.h */, 4F86BF9019F01234007A3D4A /* MessageTableViewCell.m */, 4F7C6BA31A6E208E006E3FAB /* MessageTextView.h */, 4F7C6BA41A6E208E006E3FAB /* MessageTextView.m */, 4FF04E571B3F7A04004C3BED /* TypingIndicatorView.h */, 4FF04E581B3F7A04004C3BED /* TypingIndicatorView.m */, 4F8ADA751A68C37400023752 /* Message.h */, 4F8ADA761A68C37400023752 /* Message.m */, 4F86BF8E19F01234007A3D4A /* Images.xcassets */, 4F86BFE719F05519007A3D4A /* Bridge-Header.h */, ); name = Shared; path = "Messenger-Shared"; sourceTree = ""; }; 4F86BFC419F050AF007A3D4A /* Swift Example */ = { isa = PBXGroup; children = ( 4F86BFC719F050AF007A3D4A /* AppDelegate.swift */, F5A1CC971C542EB600908ECB /* MessageViewController.swift */, 4F86BFCB19F050AF007A3D4A /* Main.storyboard */, 4F86BFD019F050AF007A3D4A /* LaunchScreen.xib */, 4F86BFC519F050AF007A3D4A /* Supporting Files */, ); name = "Swift Example"; path = "Messenger-Swift"; sourceTree = ""; }; 4F86BFC519F050AF007A3D4A /* Supporting Files */ = { isa = PBXGroup; children = ( 4F86BFC619F050AF007A3D4A /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; 68B0EE14916D384C7E703B9B /* Pods */ = { isa = PBXGroup; children = ( 622BF1F6C9CF013275EF6275 /* Pods.debug.xcconfig */, C905E5DB18A83F74BD092CDD /* Pods.release.xcconfig */, ); name = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 4F3EDB48199ED00F004C15D6 /* Messenger-Programatic */ = { isa = PBXNativeTarget; buildConfigurationList = 4F3EDB7E199ED00F004C15D6 /* Build configuration list for PBXNativeTarget "Messenger-Programatic" */; buildPhases = ( CDD847F204B5437180FB97BB /* Check Pods Manifest.lock */, 4F3EDB45199ED00F004C15D6 /* Sources */, 4F3EDB46199ED00F004C15D6 /* Frameworks */, 4F3EDB47199ED00F004C15D6 /* Resources */, 598FDC5936E74A05A38CC639 /* Copy Pods Resources */, D8BE09D94DEBD010C0FEBAD1 /* Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "Messenger-Programatic"; productName = ChatRoom; productReference = 4F3EDB49199ED00F004C15D6 /* Messenger.app */; productType = "com.apple.product-type.application"; }; 4F86BF6419F011D0007A3D4A /* Messenger-Storyboard */ = { isa = PBXNativeTarget; buildConfigurationList = 4F86BF8919F011D0007A3D4A /* Build configuration list for PBXNativeTarget "Messenger-Storyboard" */; buildPhases = ( A3DCCD9EDC51111004739174 /* Check Pods Manifest.lock */, 4F86BF6119F011D0007A3D4A /* Sources */, 4F86BF6219F011D0007A3D4A /* Frameworks */, 4F86BF6319F011D0007A3D4A /* Resources */, AC349F540F6112E2363205C8 /* Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = "Messenger-Storyboard"; productName = "Messenger-Storyboard"; productReference = 4F86BF6519F011D0007A3D4A /* Messenger-Storyboard.app */; productType = "com.apple.product-type.application"; }; 4F86BFC219F050AF007A3D4A /* Messenger-Swift */ = { isa = PBXNativeTarget; buildConfigurationList = 4F86BFE319F050AF007A3D4A /* Build configuration list for PBXNativeTarget "Messenger-Swift" */; buildPhases = ( 5EA63F26E6C2C54F5E2EC192 /* Check Pods Manifest.lock */, 4F86BFBF19F050AF007A3D4A /* Sources */, 4F86BFC019F050AF007A3D4A /* Frameworks */, 4F86BFC119F050AF007A3D4A /* Resources */, EC6E1A5473DEB830A89350DB /* Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = "Messenger-Swift"; productName = "Messenger-Swift"; productReference = 4F86BFC319F050AF007A3D4A /* Messenger-Swift.app */; productType = "com.apple.product-type.application"; }; 71BE10011A72F3130083EE32 /* Messenger-iPad-Sheet */ = { isa = PBXNativeTarget; buildConfigurationList = 71BE10221A72F3130083EE32 /* Build configuration list for PBXNativeTarget "Messenger-iPad-Sheet" */; buildPhases = ( 8D98662D95A755657E54323F /* Check Pods Manifest.lock */, 71BE0FFE1A72F3130083EE32 /* Sources */, 71BE0FFF1A72F3130083EE32 /* Frameworks */, 71BE10001A72F3130083EE32 /* Resources */, 57A4299BBA1384EEA0F08157 /* Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = "Messenger-iPad-Sheet"; productName = "Messenger-Programatic-iPad-Sheet"; productReference = 71BE10021A72F3130083EE32 /* Messenger-iPad-Sheet.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 4F3EDB41199ED00F004C15D6 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; LastUpgradeCheck = 0900; ORGANIZATIONNAME = "Slack Technologies, Inc."; TargetAttributes = { 4F3EDB48199ED00F004C15D6 = { DevelopmentTeam = S6MX3RA9ZM; }; 4F86BF6419F011D0007A3D4A = { CreatedOnToolsVersion = 6.0.1; DevelopmentTeam = S6MX3RA9ZM; }; 4F86BFC219F050AF007A3D4A = { CreatedOnToolsVersion = 6.0.1; DevelopmentTeam = S6MX3RA9ZM; LastSwiftMigration = 0800; }; 71BE10011A72F3130083EE32 = { CreatedOnToolsVersion = 6.1.1; }; }; }; buildConfigurationList = 4F3EDB44199ED00F004C15D6 /* Build configuration list for PBXProject "Messenger" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 4F3EDB40199ED00F004C15D6; productRefGroup = 4F3EDB4A199ED00F004C15D6 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 4F3EDB48199ED00F004C15D6 /* Messenger-Programatic */, 4F86BF6419F011D0007A3D4A /* Messenger-Storyboard */, 4F86BFC219F050AF007A3D4A /* Messenger-Swift */, 71BE10011A72F3130083EE32 /* Messenger-iPad-Sheet */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 4F3EDB47199ED00F004C15D6 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F5DE01401B9679C4005E9082 /* LaunchScreen.xib in Resources */, 4F86BF9319F01234007A3D4A /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 4F86BF6319F011D0007A3D4A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4F86BF7319F011D0007A3D4A /* Main.storyboard in Resources */, 4F86BF9419F01234007A3D4A /* Images.xcassets in Resources */, 4F86BF7819F011D0007A3D4A /* LaunchScreen.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 4F86BFC119F050AF007A3D4A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4F86BFE519F050DD007A3D4A /* Images.xcassets in Resources */, 4F86BFCD19F050AF007A3D4A /* Main.storyboard in Resources */, 4F86BFD219F050AF007A3D4A /* LaunchScreen.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 71BE10001A72F3130083EE32 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 71BE102F1A72F42F0083EE32 /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 57A4299BBA1384EEA0F08157 /* Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; showEnvVarsInLog = 0; }; 598FDC5936E74A05A38CC639 /* Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; showEnvVarsInLog = 0; }; 5EA63F26E6C2C54F5E2EC192 /* Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Check Pods Manifest.lock"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; 8D98662D95A755657E54323F /* Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Check Pods Manifest.lock"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; A3DCCD9EDC51111004739174 /* Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Check Pods Manifest.lock"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; AC349F540F6112E2363205C8 /* Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; showEnvVarsInLog = 0; }; CDD847F204B5437180FB97BB /* Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Check Pods Manifest.lock"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; D8BE09D94DEBD010C0FEBAD1 /* Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; EC6E1A5473DEB830A89350DB /* Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 4F3EDB45199ED00F004C15D6 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4F86BF9719F01234007A3D4A /* MessageViewController.m in Sources */, 4F3EDB5D199ED00F004C15D6 /* AppDelegate.m in Sources */, 4F8ADA771A68C37400023752 /* Message.m in Sources */, 4F86BF9519F01234007A3D4A /* MessageTableViewCell.m in Sources */, 4FF04E591B3F7A04004C3BED /* TypingIndicatorView.m in Sources */, 4F3EDB59199ED00F004C15D6 /* main.m in Sources */, 4F32561E1AA77431008C1DD9 /* MessageTextView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 4F86BF6119F011D0007A3D4A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4F9DFA7F1AD4EDB900841D98 /* ViewController.m in Sources */, 71BE10301A76C4890083EE32 /* MessageTextView.m in Sources */, 4F86BF9819F01234007A3D4A /* MessageViewController.m in Sources */, 4F86BF9619F01234007A3D4A /* MessageTableViewCell.m in Sources */, 4F8ADA781A68C37400023752 /* Message.m in Sources */, 4F86BF6A19F011D0007A3D4A /* main.m in Sources */, 4F32561C1AA77427008C1DD9 /* AppDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 4F86BFBF19F050AF007A3D4A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 9F4466C31C9A1B6B0001609A /* MessageTableViewCell.m in Sources */, F5CD59211C9FE0EC00DD9A4E /* MessageViewController.swift in Sources */, 9F4466C51C9A1B6B0001609A /* MessageTextView.m in Sources */, 9F4466C71C9A1B6B0001609A /* TypingIndicatorView.m in Sources */, 4F86BFCA19F050AF007A3D4A /* (null) in Sources */, 4F8ADA791A68C37400023752 /* Message.m in Sources */, 4F86BFC819F050AF007A3D4A /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 71BE0FFE1A72F3130083EE32 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 71BE10281A72F3E50083EE32 /* MessageViewController.m in Sources */, 71BE10291A72F3E50083EE32 /* MessageTableViewCell.m in Sources */, 71BE102B1A72F3E50083EE32 /* Message.m in Sources */, 4F32561B1AA77386008C1DD9 /* main.m in Sources */, 4F3256191AA77386008C1DD9 /* AppDelegate.m in Sources */, 4F32561D1AA77430008C1DD9 /* MessageTextView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 4F86BF7119F011D0007A3D4A /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 4F86BF7219F011D0007A3D4A /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 4F86BF7619F011D0007A3D4A /* LaunchScreen.xib */ = { isa = PBXVariantGroup; children = ( 4F86BF7719F011D0007A3D4A /* Base */, ); name = LaunchScreen.xib; sourceTree = ""; }; 4F86BFCB19F050AF007A3D4A /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 4F86BFCC19F050AF007A3D4A /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 4F86BFD019F050AF007A3D4A /* LaunchScreen.xib */ = { isa = PBXVariantGroup; children = ( 4F86BFD119F050AF007A3D4A /* Base */, ); name = LaunchScreen.xib; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 4F3EDB7C199ED00F004C15D6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 4F3EDB7D199ED00F004C15D6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 4F3EDB7F199ED00F004C15D6 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 622BF1F6C9CF013275EF6275 /* Pods.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_ENABLE_MODULES = YES; CLANG_STATIC_ANALYZER_MODE = deep; CODE_SIGN_IDENTITY = "iPhone Developer"; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = ""; INFOPLIST_FILE = "Messenger-Programatic/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.slack.Messenger; PRODUCT_NAME = Messenger; PROVISIONING_PROFILE = ""; RUN_CLANG_STATIC_ANALYZER = YES; SWIFT_OBJC_BRIDGING_HEADER = "Messenger-Swift/Messenger-Programatic-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; WRAPPER_EXTENSION = app; }; name = Debug; }; 4F3EDB80199ED00F004C15D6 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = C905E5DB18A83F74BD092CDD /* Pods.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_ENABLE_MODULES = YES; CLANG_STATIC_ANALYZER_MODE = deep; CODE_SIGN_IDENTITY = "iPhone Developer"; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = ""; INFOPLIST_FILE = "Messenger-Programatic/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.slack.Messenger; PRODUCT_NAME = Messenger; PROVISIONING_PROFILE = ""; RUN_CLANG_STATIC_ANALYZER = YES; SWIFT_OBJC_BRIDGING_HEADER = "Messenger-Swift/Messenger-Programatic-Bridging-Header.h"; WRAPPER_EXTENSION = app; }; name = Release; }; 4F86BF8519F011D0007A3D4A /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 622BF1F6C9CF013275EF6275 /* Pods.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_STATIC_ANALYZER_MODE = deep; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = "Messenger-Storyboard/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = com.slack.Messenger; PRODUCT_NAME = "$(TARGET_NAME)"; RUN_CLANG_STATIC_ANALYZER = YES; }; name = Debug; }; 4F86BF8619F011D0007A3D4A /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = C905E5DB18A83F74BD092CDD /* Pods.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_STATIC_ANALYZER_MODE = deep; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; ENABLE_STRICT_OBJC_MSGSEND = YES; INFOPLIST_FILE = "Messenger-Storyboard/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = com.slack.Messenger; PRODUCT_NAME = "$(TARGET_NAME)"; RUN_CLANG_STATIC_ANALYZER = YES; }; name = Release; }; 4F86BFDF19F050AF007A3D4A /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 622BF1F6C9CF013275EF6275 /* Pods.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_STATIC_ANALYZER_MODE = deep; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = "Messenger-Swift/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = com.slack.Messenger; PRODUCT_NAME = "$(TARGET_NAME)"; RUN_CLANG_STATIC_ANALYZER = YES; SWIFT_OBJC_BRIDGING_HEADER = "Messenger-Shared/Bridge-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 3.0; }; name = Debug; }; 4F86BFE019F050AF007A3D4A /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = C905E5DB18A83F74BD092CDD /* Pods.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_STATIC_ANALYZER_MODE = deep; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; ENABLE_STRICT_OBJC_MSGSEND = YES; INFOPLIST_FILE = "Messenger-Swift/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = com.slack.Messenger; PRODUCT_NAME = "$(TARGET_NAME)"; RUN_CLANG_STATIC_ANALYZER = YES; SWIFT_OBJC_BRIDGING_HEADER = "Messenger-Shared/Bridge-Header.h"; SWIFT_VERSION = 3.0; }; name = Release; }; 71BE10231A72F3130083EE32 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 622BF1F6C9CF013275EF6275 /* Pods.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_WARN_UNREACHABLE_CODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = "$(SRCROOT)/Messenger-iPad-Sheet/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = com.slack.Messenger; PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = 2; }; name = Debug; }; 71BE10241A72F3130083EE32 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = C905E5DB18A83F74BD092CDD /* Pods.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_WARN_UNREACHABLE_CODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; INFOPLIST_FILE = "$(SRCROOT)/Messenger-iPad-Sheet/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = com.slack.Messenger; PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = 2; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 4F3EDB44199ED00F004C15D6 /* Build configuration list for PBXProject "Messenger" */ = { isa = XCConfigurationList; buildConfigurations = ( 4F3EDB7C199ED00F004C15D6 /* Debug */, 4F3EDB7D199ED00F004C15D6 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 4F3EDB7E199ED00F004C15D6 /* Build configuration list for PBXNativeTarget "Messenger-Programatic" */ = { isa = XCConfigurationList; buildConfigurations = ( 4F3EDB7F199ED00F004C15D6 /* Debug */, 4F3EDB80199ED00F004C15D6 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 4F86BF8919F011D0007A3D4A /* Build configuration list for PBXNativeTarget "Messenger-Storyboard" */ = { isa = XCConfigurationList; buildConfigurations = ( 4F86BF8519F011D0007A3D4A /* Debug */, 4F86BF8619F011D0007A3D4A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 4F86BFE319F050AF007A3D4A /* Build configuration list for PBXNativeTarget "Messenger-Swift" */ = { isa = XCConfigurationList; buildConfigurations = ( 4F86BFDF19F050AF007A3D4A /* Debug */, 4F86BFE019F050AF007A3D4A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 71BE10221A72F3130083EE32 /* Build configuration list for PBXNativeTarget "Messenger-iPad-Sheet" */ = { isa = XCConfigurationList; buildConfigurations = ( 71BE10231A72F3130083EE32 /* Debug */, 71BE10241A72F3130083EE32 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 4F3EDB41199ED00F004C15D6 /* Project object */; } ================================================ FILE: Examples/Messenger.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Examples/Messenger.xcodeproj/project.xcworkspace/xcshareddata/Messenger.xccheckout ================================================ IDESourceControlProjectFavoriteDictionaryKey IDESourceControlProjectIdentifier 199D9F17-B5FE-4731-869F-F59274123773 IDESourceControlProjectName Messenger IDESourceControlProjectOriginsDictionary 8E9694F087F70D8D068B66654B1CC8608B5F6B2D https://github.com/ThumbWorks/SlackTextViewController.git IDESourceControlProjectPath Examples/Messenger/Messenger.xcodeproj IDESourceControlProjectRelativeInstallPathDictionary 8E9694F087F70D8D068B66654B1CC8608B5F6B2D ../../../.. IDESourceControlProjectURL https://github.com/ThumbWorks/SlackTextViewController.git IDESourceControlProjectVersion 111 IDESourceControlProjectWCCIdentifier 8E9694F087F70D8D068B66654B1CC8608B5F6B2D IDESourceControlProjectWCConfigurations IDESourceControlRepositoryExtensionIdentifierKey public.vcs.git IDESourceControlWCCIdentifierKey 8E9694F087F70D8D068B66654B1CC8608B5F6B2D IDESourceControlWCCName TWSlackTextViewController ================================================ FILE: Examples/Messenger.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Examples/Podfile ================================================ source 'https://github.com/CocoaPods/Specs.git' workspace 'Messenger.xcworkspace' platform :ios, '7.0' def shared_pods pod 'SlackTextViewController', :path => '../SlackTextViewController.podspec' pod 'LoremIpsum', '~> 1.0' end target 'Messenger-Programatic' do shared_pods end target 'Messenger-Storyboard' do shared_pods end target 'Messenger-Swift' do shared_pods end target 'Messenger-iPad-Sheet' do shared_pods end ================================================ FILE: Examples/Pods/Local Podspecs/SlackTextViewController.podspec.json ================================================ { "name": "SlackTextViewController", "version": "1.9.1", "summary": "A drop-in UIViewController subclass with a custom growing text input and other useful messaging features.", "description": "Meant to be a replacement for UITableViewController & UICollectionViewController. This library is used in Slack's iOS app. It was built to fit our needs, but is flexible enough to be reused by others wanting to build great messaging apps for iOS.", "homepage": "https://slack.com/", "screenshots": "https://github.com/slackhq/SlackTextViewController/raw/master/Screenshots/slacktextviewcontroller_demo.gif", "license": { "type": "MIT", "file": "LICENSE" }, "authors": { "Slack Technologies, Inc.": "ios-team@slack-corp.com" }, "source": { "git": "https://github.com/slackhq/SlackTextViewController.git", "tag": "v1.9.1" }, "platforms": { "ios": "7.0" }, "requires_arc": true, "header_mappings_dir": "Source", "source_files": "Source/**/*.{h,m}" } ================================================ FILE: Examples/Pods/LoremIpsum/License.markdown ================================================ Copyright (c) 2013 Lukas Kubanek. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Examples/Pods/LoremIpsum/LoremIpsum/LoremIpsum.h ================================================ /* * __ ____ * / / ____ ________ ____ ___ / _/___ _______ ______ ___ * / / / __ \/ ___/ _ \/ __ `__ \ / // __ \/ ___/ / / / __ `__ \ * / /___/ /_/ / / / __/ / / / / / _/ // /_/ (__ ) /_/ / / / / / / * /_____/\____/_/ \___/_/ /_/ /_/ /___/ .___/____/\__,_/_/ /_/ /_/ * /_/ * * LoremIpsum.h * http://github.com/lukaskubanek/LoremIpsum * 2013-2014 (c) Lukas Kubanek (http://lukaskubanek.com) */ #import #if TARGET_OS_IPHONE #import #elif TARGET_OS_MAC #import #endif typedef NS_ENUM(NSInteger, LIPlaceholderImageService) { LIPlaceholderImageServiceLoremPixel, LIPlaceholderImageServiceHhhhold, LIPlaceholderImageServiceDummyImage, LIPlaceholderImageServicePlaceKitten, LIPlaceholderImageServiceDefault = LIPlaceholderImageServiceLoremPixel }; @interface LoremIpsum : NSObject ///------------------------------- /// @name Texts ///------------------------------- /** * Generates a random single word. */ + (NSString *)word; /** * Generates random words separated by a space character. * * @param numberOfWords The number of generated words. */ + (NSString *)wordsWithNumber:(NSInteger)numberOfWords; /** * Generates a random sentence. */ + (NSString *)sentence; /** * Generates random concatenated sentences. * * @param numberOfSentences The number of generated sentences. */ + (NSString *)sentencesWithNumber:(NSInteger)numberOfSentences; /** * Generates a random paragraph with multiple sentences. */ + (NSString *)paragraph; /** * Generates random paragraphs joined by a new line character. * * @param numberOfParagraphs The number of generated paragraphs. */ + (NSString *)paragraphsWithNumber:(NSInteger)numberOfParagraphs; /** * Generates a random title. */ + (NSString *)title; ///------------------------------- /// @name Misc Data ///------------------------------- /** * Generates a random name consisting of a first and a last name. */ + (NSString *)name; /** * Generates a random first name. */ + (NSString *)firstName; /** * Generates a random last name. */ + (NSString *)lastName; /** * Generates a random email address. */ + (NSString *)email; /** * Generates a random URL address with the HTTP prefix. */ + (NSURL *)URL; /** * Generates a sample tweet with 140 characters. */ + (NSString *)tweet; /** * Generates a random date and time within the last 4 years. */ + (NSDate *)date; ///------------------------------- /// @name Images ///------------------------------- #if TARGET_OS_IPHONE /** * Returns an URL for a placeholder image with the given size. * * @param size The desired size of the image. */ + (NSURL *)URLForPlaceholderImageWithSize:(CGSize)size; /** * Returns an URL for a placeholder image from the given image service and with the given size. * * @param service The image service. * @param size The desired size of the image. */ + (NSURL *)URLForPlaceholderImageFromService:(LIPlaceholderImageService)service withSize:(CGSize)size; /** * Returns a placeholder image with the given size. * * @param size The desired size of the image. */ + (UIImage *)placeholderImageWithSize:(CGSize)size; /** * Returns a placeholder image from the given image service and with the given size. * * @param service The image service. * @param size The desired size of the image. */ + (UIImage *)placeholderImageFromService:(LIPlaceholderImageService)service withSize:(CGSize)size; /** * Asynchronously loads a placeholder image with the given size and executes the completion block. * * @param size The desired size of the image. * @param completion The completion block which is executed asynchronously after the loading the image. */ + (void)asyncPlaceholderImageWithSize:(CGSize)size completion:(void (^)(UIImage *image))completion; /** * Asynchronously loads a placeholder image from the given image service with the given size * and executes the completion block. * * @param service The image service. * @param size The desired size of the image. * @param completion The completion block which is executed asynchronously after the loading the image. */ + (void)asyncPlaceholderImageFromService:(LIPlaceholderImageService)service withSize:(CGSize)size completion:(void (^)(UIImage *image))completion; #elif TARGET_OS_MAC /** * Returns an URL for a placeholder image with the given size. * * @param size The desired size of the image. */ + (NSURL *)URLForPlaceholderImageWithSize:(NSSize)size; /** * Returns an URL for a placeholder image from the given image service and with the given size. * * @param service The image service. * @param size The desired size of the image. */ + (NSURL *)URLForPlaceholderImageFromService:(LIPlaceholderImageService)service withSize:(NSSize)size; /** * Returns a placeholder image with the given size. * * @param size The desired size of the image. */ + (NSImage *)placeholderImageWithSize:(NSSize)size; /** * Returns a placeholder image from the given image service and with the given size. * * @param service The image service. * @param size The desired size of the image. */ + (NSImage *)placeholderImageFromService:(LIPlaceholderImageService)service withSize:(NSSize)size; /** * Asynchronously loads a placeholder image with the given size and executes the completion block. * * @param size The desired size of the image. * @param completion The completion block which is executed asynchronously after the loading the image. */ + (void)asyncPlaceholderImageWithSize:(NSSize)size completion:(void (^)(NSImage *image))completion; /** * Asynchronously loads a placeholder image from the given image service with the given size * and executes the completion block. * * @param service The image service. * @param size The desired size of the image. * @param completion The completion block which is executed asynchronously after the loading the image. */ + (void)asyncPlaceholderImageFromService:(LIPlaceholderImageService)service withSize:(NSSize)size completion:(void (^)(NSImage *image))completion; #endif @end ================================================ FILE: Examples/Pods/LoremIpsum/LoremIpsum/LoremIpsum.m ================================================ /* * __ ____ * / / ____ ________ ____ ___ / _/___ _______ ______ ___ * / / / __ \/ ___/ _ \/ __ `__ \ / // __ \/ ___/ / / / __ `__ \ * / /___/ /_/ / / / __/ / / / / / _/ // /_/ (__ ) /_/ / / / / / / * /_____/\____/_/ \___/_/ /_/ /_/ /___/ .___/____/\__,_/_/ /_/ /_/ * /_/ * * LoremIpsum.m * http://github.com/lukaskubanek/LoremIpsum * 2013-2014 (c) Lukas Kubanek (http://lukaskubanek.com) */ #import "LoremIpsum.h" #if TARGET_OS_IPHONE typedef UIImage LIImage; typedef CGSize LISize; #elif TARGET_OS_MAC typedef NSImage LIImage; typedef NSSize LISize; #endif NSUInteger const LINumberOfLastYears = 4; NSUInteger const LIMinNumberOfWordsInSentence = 4; NSUInteger const LIMaxNumberOfWordsInSentence = 16; NSUInteger const LIMinNumberOfSentencesInParagraph = 3; NSUInteger const LIMaxNumberOfSentencesInParagraph = 9; NSUInteger const LIMinNumberOfWordsInTitle = 2; NSUInteger const LIMaxNumberOfWordsInTitle = 7; NSUInteger LIRandomUnsignedInteger(NSUInteger lowerBound, NSUInteger upperBound) { return arc4random() % (upperBound - lowerBound) + lowerBound; } @implementation NSArray (LoremIpsum) - (id)li_randomObject { return [self objectAtIndex:LIRandomUnsignedInteger(0, [self count])]; } @end @implementation NSString (LoremIpsum) - (NSString *)li_stringByCapitalizingFirstLetter { NSString *capitalizedFirstLetter = [[self substringToIndex:1] capitalizedString]; return [self stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:capitalizedFirstLetter]; } @end @implementation LoremIpsum #pragma mark - Content Database + (NSArray *)words { static NSArray *words = nil; if (!words) { words = [@"alias consequatur aut perferendis sit voluptatem accusantium doloremque aperiam eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo aspernatur aut odit aut fugit sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt neque dolorem ipsum quia dolor sit amet consectetur adipisci velit sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem ut enim ad minima veniam quis nostrum exercitationem ullam corporis nemo enim ipsam voluptatem quia voluptas sit suscipit laboriosam nisi ut aliquid ex ea commodi consequatur quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae et iusto odio dignissimos ducimus qui blanditiis praesentium laudantium totam rem voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident sed ut perspiciatis unde omnis iste natus error similique sunt in culpa qui officia deserunt mollitia animi id est laborum et dolorum fuga et harum quidem rerum facilis est et expedita distinctio nam libero tempore cum soluta nobis est eligendi optio cumque nihil impedit quo porro quisquam est qui minus id quod maxime placeat facere possimus omnis voluptas assumenda est omnis dolor repellendus temporibus autem quibusdam et aut consequatur vel illum qui dolorem eum fugiat quo voluptas nulla pariatur at vero eos et accusamus officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae itaque earum rerum hic tenetur a sapiente delectus ut aut reiciendis voluptatibus maiores doloribus asperiores repellat" componentsSeparatedByString:@" "]; } return words; } + (NSArray *)firstNames { static NSArray *firstNames = nil; if (!firstNames) { firstNames = [@"Judith Angelo Margarita Kerry Elaine Lorenzo Justice Doris Raul Liliana Kerry Elise Ciaran Johnny Moses Davion Penny Mohammed Harvey Sheryl Hudson Brendan Brooklynn Denis Sadie Trisha Jacquelyn Virgil Cindy Alexa Marianne Giselle Casey Alondra Angela Katherine Skyler Kyleigh Carly Abel Adrianna Luis Dominick Eoin Noel Ciara Roberto Skylar Brock Earl Dwayne Jackie Hamish Sienna Nolan Daren Jean Shirley Connor Geraldine Niall Kristi Monty Yvonne Tammie Zachariah Fatima Ruby Nadia Anahi Calum Peggy Alfredo Marybeth Bonnie Gordon Cara John Staci Samuel Carmen Rylee Yehudi Colm Beth Dulce Darius inley Javon Jason Perla Wayne Laila Kaleigh Maggie Don Quinn Collin Aniya Zoe Isabel Clint Leland Esmeralda Emma Madeline Byron Courtney Vanessa Terry Antoinette George Constance Preston Rolando Caleb Kenneth Lynette Carley Francesca Johnnie Jordyn Arturo Camila Skye Guy Ana Kaylin Nia Colton Bart Brendon Alvin Daryl Dirk Mya Pete Joann Uriel Alonzo Agnes Chris Alyson Paola Dora Elias Allen Jackie Eric Bonita Kelvin Emiliano Ashton Kyra Kailey Sonja Alberto Ty Summer Brayden Lori Kelly Tomas Joey Billie Katie Stephanie Danielle Alexis Jamal Kieran Lucinda Eliza Allyson Melinda Alma Piper Deana Harriet Bryce Eli Jadyn Rogelio Orlaith Janet Randal Toby Carla Lorie Caitlyn Annika Isabelle inn Ewan Maisie Michelle Grady Ida Reid Emely Tricia Beau Reese Vance Dalton Lexi Rafael Makenzie Mitzi Clinton Xena Angelina Kendrick Leslie Teddy Jerald Noelle Neil Marsha Gayle Omar Abigail Alexandra Phil Andre Billy Brenden Bianca Jared Gretchen Patrick Antonio Josephine Kyla Manuel Freya Kellie Tonia Jamie Sydney Andres Ruben Harrison Hector Clyde Wendell Kaden Ian Tracy Cathleen Shawn" componentsSeparatedByString:@" "]; } return firstNames; } + (NSArray *)lastNames { static NSArray *lastNames = nil; if (!lastNames) { lastNames = [@"Chung Chen Melton Hill Puckett Song Hamilton Bender Wagner McLaughlin McNamara Raynor Moon Woodard Desai Wallace Lawrence Griffin Dougherty Powers May Steele Teague Vick Gallagher Solomon Walsh Monroe Connolly Hawkins Middleton Goldstein Watts Johnston Weeks Wilkerson Barton Walton Hall Ross Chung Bender Woods Mangum Joseph Rosenthal Bowden Barton Underwood Jones Baker Merritt Cross Cooper Holmes Sharpe Morgan Hoyle Allen Rich Rich Grant Proctor Diaz Graham Watkins Hinton Marsh Hewitt Branch Walton O'Brien Case Watts Christensen Parks Hardin Lucas Eason Davidson Whitehead Rose Sparks Moore Pearson Rodgers Graves Scarborough Sutton Sinclair Bowman Olsen Love McLean Christian Lamb James Chandler Stout Cowan Golden Bowling Beasley Clapp Abrams Tilley Morse Boykin Sumner Cassidy Davidson Heath Blanchard McAllister McKenzie Byrne Schroeder Griffin Gross Perkins Robertson Palmer Brady Rowe Zhang Hodge Li Bowling Justice Glass Willis Hester Floyd Graves Fischer Norman Chan Hunt Byrd Lane Kaplan Heller May Jennings Hanna Locklear Holloway Jones Glover Vick O'Donnell Goldman McKenna Starr Stone McClure Watson Monroe Abbott Singer Hall Farrell Lucas Norman Atkins Monroe Robertson Sykes Reid Chandler Finch Hobbs Adkins Kinney Whitaker Alexander Conner Waters Becker Rollins Love Adkins Black Fox Hatcher Wu Lloyd Joyce Welch Matthews Chappell MacDonald Kane Butler Pickett Bowman Barton Kennedy Branch Thornton McNeill Weinstein Middleton Moss Lucas Rich Carlton Brady Schultz Nichols Harvey Stevenson Houston Dunn West O'Brien Barr Snyder Cain Heath Boswell Olsen Pittman Weiner Petersen Davis Coleman Terrell Norman Burch Weiner Parrott Henry Gray Chang McLean Eason Weeks Siegel Puckett Heath Hoyle Garrett Neal Baker Goldman Shaffer Choi Carver" componentsSeparatedByString:@" "]; } return lastNames; } + (NSArray *)emailDomains { static NSArray *emailDomains = nil; if (!emailDomains) { emailDomains = [@"gmail.com yahoo.com hotmail.com email.com live.com me.com mac.com aol.com fastmail.com mail.com" componentsSeparatedByString:@" "]; } return emailDomains; } + (NSArray *)domains { static NSArray *domains = nil; if (!domains) { domains = [@"twitter.com google.com youtube.com wordpress.org adobe.com blogspot.com godaddy.com wikipedia.org wordpress.com yahoo.com linkedin.com amazon.com flickr.com w3.org apple.com myspace.com tumblr.com digg.com microsoft.com vimeo.com pinterest.com qq.com stumbleupon.com youtu.be addthis.com miibeian.gov.cn delicious.com baidu.com feedburner.com bit.ly" componentsSeparatedByString:@" "]; } return domains; } #pragma mark - Texts + (NSString *)word { return [self wordsWithNumber:1]; } + (NSString *)wordsWithNumber:(NSInteger)numberOfWords { NSAssert(numberOfWords > 0, @"The number of words has to be greater than zero."); NSMutableArray *words = [NSMutableArray arrayWithCapacity:(NSUInteger)numberOfWords]; for (NSInteger i = 0; i < numberOfWords; i++) { [words addObject:[[self words] li_randomObject]]; } return [words componentsJoinedByString:@" "]; } + (NSString *)sentence { return [self sentencesWithNumber:1]; } + (NSString *)sentencesWithNumber:(NSInteger)numberOfSentences { NSAssert(numberOfSentences > 0, @"The number of sentences has to be greater than zero."); NSMutableArray *sentences = [NSMutableArray arrayWithCapacity:(NSUInteger)numberOfSentences]; for (NSInteger i = 0; i < numberOfSentences; i++) { NSInteger numberOfWords = LIRandomUnsignedInteger(LIMinNumberOfWordsInSentence, LIMaxNumberOfWordsInSentence); NSString *sentence = [[self wordsWithNumber:numberOfWords] li_stringByCapitalizingFirstLetter]; [sentences addObject:sentence]; } return [[sentences componentsJoinedByString:@". "] stringByAppendingString:@"."]; } + (NSString *)paragraph { return [self paragraphsWithNumber:1]; } + (NSString *)paragraphsWithNumber:(NSInteger)numberOfParagraphs { NSAssert(numberOfParagraphs > 0, @"The number of paragraphs has to be greater than zero."); NSMutableArray *paragraphs = [NSMutableArray arrayWithCapacity:(NSUInteger)numberOfParagraphs]; for (NSInteger i = 0; i < numberOfParagraphs; i++) { NSInteger numberOfSentences = LIRandomUnsignedInteger(LIMinNumberOfSentencesInParagraph, LIMaxNumberOfSentencesInParagraph); [paragraphs addObject:[self sentencesWithNumber:numberOfSentences]]; } return [paragraphs componentsJoinedByString:@"\n"]; } + (NSString *)title { NSInteger number0fWords = LIRandomUnsignedInteger(LIMinNumberOfWordsInTitle, LIMaxNumberOfWordsInTitle); return [[self wordsWithNumber:number0fWords] capitalizedString]; } #pragma mark - Misc Data + (NSString *)name { return [NSString stringWithFormat:@"%@ %@", [self firstName], [self lastName]]; } + (NSString *)firstName { return [[self firstNames] li_randomObject]; } + (NSString *)lastName { return [[self lastNames] li_randomObject]; } + (NSString *)email { NSString *domain = [[self emailDomains] li_randomObject]; NSString *delimiter = [@[@"", @".", @"-", @"_"] li_randomObject]; return [[[[[[self firstName] stringByAppendingString:delimiter] stringByAppendingString:[self lastName]] stringByAppendingString:@"@"] stringByAppendingString:domain] lowercaseString]; } + (NSURL *)URL { return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/", [[self domains] li_randomObject]]]; } /* source: http://www.kevadamson.com/talking-of-design/article/140-alternative-characters-to-lorem-ipsum */ + (NSString *)tweet { NSArray *tweets = @[ @"Far away, in a forest next to a river beneath the mountains, there lived a small purple otter called Philip. Philip likes sausages. The End.", @"He liked the quality sausages from Marks & Spencer but due to the recession he had been forced to shop in a less desirable supermarket. End.", @"He awoke one day to find his pile of sausages missing. Roger the greedy boar with human eyes, had skateboarded into the forest & eaten them!" ]; return [tweets li_randomObject]; } + (NSDate *)date { NSDate *currentDate = [NSDate date]; NSDateComponents *referenceDateComponents = [[NSDateComponents alloc] init]; [referenceDateComponents setYear:-LINumberOfLastYears]; NSDate *referenceDate = [[NSCalendar currentCalendar] dateByAddingComponents:referenceDateComponents toDate:currentDate options:0]; NSTimeInterval timeIntervalSinceReferenceDate = [currentDate timeIntervalSinceDate:referenceDate]; NSTimeInterval randomTimeInterval = LIRandomUnsignedInteger(0, (NSUInteger)timeIntervalSinceReferenceDate); return [referenceDate dateByAddingTimeInterval:randomTimeInterval]; } #pragma mark - URLs for Placeholder Images + (NSURL *)URLForPlaceholderImageWithSize:(LISize)size { return [self URLForPlaceholderImageFromService:LIPlaceholderImageServiceDefault withSize:size]; } + (NSURL *)URLForPlaceholderImageFromService:(LIPlaceholderImageService)service withSize:(LISize)size { NSString *URLString; NSUInteger width = (NSUInteger)size.width; NSUInteger height = (NSUInteger)size.height; switch (service) { case LIPlaceholderImageServiceLoremPixel: { URLString = [NSString stringWithFormat:@"http://lorempixel.com/%zd/%zd/", width, height]; break; } case LIPlaceholderImageServiceHhhhold: { URLString = [NSString stringWithFormat:@"http://hhhhold.com/%zdx%zd", width, height]; break; } case LIPlaceholderImageServiceDummyImage: { URLString = [NSString stringWithFormat:@"http://dummyimage.com/%zdx%zd", width, height]; break; } case LIPlaceholderImageServicePlaceKitten: { URLString = [NSString stringWithFormat:@"http://placekitten.com/%zd/%zd/", width, height]; break; } } return [NSURL URLWithString:URLString]; } #pragma mark - Placeholder Images + (LIImage *)placeholderImageWithSize:(LISize)size { return [self placeholderImageFromService:LIPlaceholderImageServiceDefault withSize:size]; } + (LIImage *)placeholderImageFromService:(LIPlaceholderImageService)service withSize:(LISize)size { NSURL *imageURL = [self URLForPlaceholderImageFromService:service withSize:size]; NSData *imageData = [NSData dataWithContentsOfURL:imageURL]; return [[LIImage alloc] initWithData:imageData]; } + (void)asyncPlaceholderImageWithSize:(LISize)size completion:(void (^)(LIImage *LIImage))completion { [self asyncPlaceholderImageFromService:LIPlaceholderImageServiceDefault withSize:size completion:completion]; } + (void)asyncPlaceholderImageFromService:(LIPlaceholderImageService)service withSize:(LISize)size completion:(void (^)(LIImage *LIImage))completion { NSAssert(completion, @"The completion block must not be nil."); NSURL *imageURL = [self URLForPlaceholderImageFromService:service withSize:size]; NSURLRequest *request = [[NSURLRequest alloc] initWithURL:imageURL]; NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; [NSURLConnection sendAsynchronousRequest:request queue:mainQueue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { LIImage *image; if (!error) { image = [[LIImage alloc] initWithData:data]; } completion(image); }]; } @end ================================================ FILE: Examples/Pods/LoremIpsum/Readme.markdown ================================================ # Lorem Ipsum *Lorem Ipsum* is a simple lorem ipsum generator for iOS and OS X apps. It supports generating text in different formats (words, sentences, paragraphs), miscelianous data (names, URLs, dates etc.) and placeholder images for both, iOS and OS X. This project was inspired by a great static site generator [Middleman](http://github.com/middleman/middleman). Its [lorem ipsum feature](https://github.com/middleman/middleman/blob/37e22c040ebcabed0ac3d8bce85aa085177d012a/middleman-core/lib/middleman-more/extensions/lorem.rb) was also a data source for this project. #### Table of Contents - [Installation](#installation) - [Usage](#usage) - [Texts](#texts) - [Misc Data](#misc-data) - [Images](#images) - [Example Projects](#example-projects) - [Author](#author) - [License](#license) ## Installation *Lorem Ipsum* can be integrated into your Xcode project using CocoaPods by putting following line into your `Podfile` and running `pod install`: ```ruby pod 'LoremIpsum', '~> 1.0' ``` Alternatively you can copy the files `LoremIpsum.h` and `LoremIpsum.m` to your project. ## Usage Wherever you want to use *Lorem Ipsum* functionality, import the header file. ```objective-c #import "LoremIpsum.h" ``` ### Texts ```objective-c NSString *word = [LoremIpsum word]; => "voluptatem" NSString *words = [LoremIpsum wordsWithNumber:5]; => "est dolores voluptatem cumque itaque" NSString *sentence = [LoremIpsum sentence]; => "Facilis magni autem ut iure cum voluptas excepturi eos." NSString *sentences = [LoremIpsum sentencesWithNumber:2]; NSString *paragraph = [LoremIpsum paragraph]; NSString *paragraphs = [LoremIpsum paragraphsWithNumber:2]; NSString *title = [LoremIpsum title]; => "Et Quibusdam" ``` ### Misc Data ```objective-c NSString *firstName = [LoremIpsum firstName]; => "Dora" NSString *lastName = [LoremIpsum lastName]; => "Glass" NSString *name = [LoremIpsum name]; => "Tomas Beasley" NSString *email = [LoremIpsum email]; => "jared.finch@hotmail.com" NSURL *URL = [LoremIpsum URL]; => "http://stumbleupon.com/" NSString *tweet = [LoremIpsum tweet]; NSDate *date = [LoremIpsum date]; ``` ### Images *Lorem Ipsum* supports creating placeholder images by downloading them from following supported services: | Service | Value of `LIPlaceholderImageService` | | ----------------------------------------- | -------------------------------------- | | [lorempixel.com](http://lorempixel.com) | `LIPlaceholderImageServiceLoremPixel` | | [hhhhold.com](http://hhhhold.com) | `LIPlaceholderImageServiceHhhhold` | | [dummyimage.com](http://dummyimage.com) | `LIPlaceholderImageServiceDummyImage` | | [placekitten.com](http://placekitten.com) | `LIPlaceholderImageServicePlaceKitten` | *Lorem Ipsum* offers methods for synchronous and asynchronous downloading of images as well as retrieving the URL for an image without downloading it. All methods work for both platforms, iOS and OS X. According to the platform `UIImage` or `NSImage` instances are created. #### Synchronous Downloading ```objective-c UIImage *image = [LoremIpsum placeholderImageFromService:LIPlaceholderImageServicePlaceKitten withSize:CGSizeMake(250, 250)]; NSImage *image = [LoremIpsum placeholderImageWithSize:NSMakeSize(100, 400)]; ``` #### Asynchronous Downloading ```objective-c [LoremIpsum asyncPlaceholderImageFromService:LIPlaceholderImageServicePlaceKitten withSize:CGSizeMake(250, 250) completion:^(UIImage *image) { // use image here }]; [LoremIpsum asyncPlaceholderImageWithSize:NSMakeSize(100, 400) completion:^(NSImage *image) { // use image here }]; ``` #### Image URLs ```objective-c NSURL *URL = [LoremIpsum URLForPlaceholderImageFromService:LIPlaceholderImageServiceDummyImage withSize:CGSizeMake(250, 250)]; ``` ## Example Projects This repository contains two [example projects](/Examples/) for each platform where the usage of *Lorem Ipsum* is shown. ![Mac OS X Example Screenshot](/Screenshot.png) ## Author | Lukas Kubanek | |------------------------------------------------------| | [lukaskubanek.com](http://lukaskubanek.com) | | [@kubanekl](https://twitter.com/kubanekl) | ## License Usage is provided under the [MIT License](http://opensource.org/licenses/MIT). See [License.markdown](License.md) for the full details. ================================================ FILE: Examples/Pods/Pods.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 1B54941E5E719905EA0976C1582AA362 /* SlackTextViewController-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BD4C1C4F8E3B6387290B1063443579F /* SlackTextViewController-dummy.m */; }; 1D87BAF6807A6F24656CE3C6E0A3D691 /* SLKTypingIndicatorView.h in Headers */ = {isa = PBXBuildFile; fileRef = E76EC386EBA8E2BD0919F33C32DBFB41 /* SLKTypingIndicatorView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2A6186F8AE6C5CD2767949046F017961 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E4E89230EF59BC255123B67864ACF77 /* Foundation.framework */; }; 3B67741D6C7B5D4C5B8AFCC351C7F424 /* UIResponder+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = FD3CEB520ABAE633DB696CD533769677 /* UIResponder+SLKAdditions.m */; }; 4D4CD125276CAEF26B7E6433C1E9A458 /* SLKTextViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C31B9359538DB1818FDCE030CB4C1695 /* SLKTextViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 52855809CE7672258D6707D8EB1DA870 /* SLKTypingIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = EC2BA9C4B342C46DFB060C7EDAC0D02E /* SLKTypingIndicatorView.m */; }; 5541B1417BA993425E1B015992908C54 /* LoremIpsum.h in Headers */ = {isa = PBXBuildFile; fileRef = D8BC2A9F3D2387D410A5C3C03BDA3E17 /* LoremIpsum.h */; settings = {ATTRIBUTES = (Public, ); }; }; 569E350F767E9D5BC2A2D44B5F629336 /* SLKTextInputbar.m in Sources */ = {isa = PBXBuildFile; fileRef = EF0E8EF6D064E81BCCF9223C6D85EBD9 /* SLKTextInputbar.m */; }; 62F2C6DA240FB3A9602CA0046869AAF2 /* Pods-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 272643F56613CA0D336AE3DBF19DC404 /* Pods-dummy.m */; }; 6C4AEEA57727B3B3D79FEBD64B156BF7 /* SLKTextView+SLKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 95ACD1202D9C5BA5ED706909142811B6 /* SLKTextView+SLKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7DB3F03605631A6742020ADBD96EE73A /* SLKTextView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 90C65F0C6E8456B1AEA0D0C5FEAE0AB0 /* SLKTextView+SLKAdditions.m */; }; 80D67571497E1D056B9AC4736F1C54E5 /* SLKUIConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = CD8D84605004C6D99DA69A6C6409F20F /* SLKUIConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; 89D3F183FA5B8BA60E82030FBA24D1E1 /* LoremIpsum.m in Sources */ = {isa = PBXBuildFile; fileRef = F99E54F177E07158903728CFCFC03D4B /* LoremIpsum.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 9303F6EC3D95E616D80AE7C30737487A /* SLKInputAccessoryView.m in Sources */ = {isa = PBXBuildFile; fileRef = F111576323FD30FF3762EBEA688A4BA5 /* SLKInputAccessoryView.m */; }; 97942BFE0D2C0ACA0A69DEC48BC044B8 /* SLKTextInputbar.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CADA432C548A9883246BF013F60DAA5 /* SLKTextInputbar.h */; settings = {ATTRIBUTES = (Public, ); }; }; 98F3BE534856D08CA2E0FC26CEE8AC1E /* UIScrollView+SLKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2544A16A1FD82EAC6B803C18D54A3F79 /* UIScrollView+SLKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C4AC3CE673950588D03A76D94CFF5D1 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E4E89230EF59BC255123B67864ACF77 /* Foundation.framework */; }; A4C9630F12E4A6AD3E9F415BE6E928B0 /* SLKInputAccessoryView.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B0ECE21021FD69590E063D96AF6304E /* SLKInputAccessoryView.h */; settings = {ATTRIBUTES = (Public, ); }; }; AA5A83ABE4694129065A4F45DBAEAC97 /* SLKTextViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F699546DA9ABB811511FC742AB1617EC /* SLKTextViewController.m */; }; BB5DE846FF718133D67729717C4C6CC9 /* LoremIpsum-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 9A3ABBD17931BCBDF6878F4179630D30 /* LoremIpsum-dummy.m */; }; C2041517A4BA4A60D02C63E65449815D /* UIResponder+SLKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 38F8DCFB48A21AEC554454EBCAA8F121 /* UIResponder+SLKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; C875A458E52C3F05A2726D025A6CAA43 /* SLKTextView.h in Headers */ = {isa = PBXBuildFile; fileRef = A2CC2D1A4404DE89733F4F6A85259C6B /* SLKTextView.h */; settings = {ATTRIBUTES = (Public, ); }; }; CBB76607675EDA4E780C54D98CC5DF8F /* SLKTextInput.h in Headers */ = {isa = PBXBuildFile; fileRef = B864757FAA2A46B94C852923F3AD40E5 /* SLKTextInput.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC8055ECB365838B897F70FC339A830C /* UIView+SLKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = C8AA8BA4CD934D5702F4394F583323E3 /* UIView+SLKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; DA9CF5D6ED28E84CC33F5EFF2D97F915 /* UIScrollView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = C41834960415015B9F61AFB584936219 /* UIScrollView+SLKAdditions.m */; }; E9015CEB577B281532777C0D97D80FA1 /* UIView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 904A23980152CBAEEEF9D965427462DD /* UIView+SLKAdditions.m */; }; EF0353E1B59D852A952B7AE54AB72200 /* SLKTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 665F6403679EDB9C5E568C75F3FDF768 /* SLKTextView.m */; }; F76DD4435CA2C94F6E441704E37D9E24 /* SLKTextInput+Implementation.m in Sources */ = {isa = PBXBuildFile; fileRef = BA32997CF782E3226A3B0F68E1D67789 /* SLKTextInput+Implementation.m */; }; F7AF382CA9A4A0680835E655B2210CEF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E4E89230EF59BC255123B67864ACF77 /* Foundation.framework */; }; FC26476E2D24910A0563D722B9BF5991 /* SLKTypingIndicatorProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1C71FEF7A4C22EEA18BBE9CBC9BE83BD /* SLKTypingIndicatorProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 63B962AF9C8829F370F8AA092B98A969 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D41D8CD98F00B204E9800998ECF8427E /* Project object */; proxyType = 1; remoteGlobalIDString = D302A4C45741884C0F17816784845B9A; remoteInfo = LoremIpsum; }; 7396AD0C520CB4738B2901477D4495E1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D41D8CD98F00B204E9800998ECF8427E /* Project object */; proxyType = 1; remoteGlobalIDString = B564AAE60BD4BB94F31E3C2A6F64164D; remoteInfo = SlackTextViewController; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 0B0ECE21021FD69590E063D96AF6304E /* SLKInputAccessoryView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SLKInputAccessoryView.h; sourceTree = ""; }; 10834806BD7B412BC24F347361FA2C8E /* Pods-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-acknowledgements.plist"; sourceTree = ""; }; 1640D683B55420448C7F631CC6AC452B /* SlackTextViewController-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SlackTextViewController-prefix.pch"; sourceTree = ""; }; 1BD4C1C4F8E3B6387290B1063443579F /* SlackTextViewController-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SlackTextViewController-dummy.m"; sourceTree = ""; }; 1C71FEF7A4C22EEA18BBE9CBC9BE83BD /* SLKTypingIndicatorProtocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SLKTypingIndicatorProtocol.h; sourceTree = ""; }; 21E917D789329016EA29D4FC962E79A2 /* SlackTextViewController.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SlackTextViewController.xcconfig; sourceTree = ""; }; 2544A16A1FD82EAC6B803C18D54A3F79 /* UIScrollView+SLKAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+SLKAdditions.h"; sourceTree = ""; }; 272643F56613CA0D336AE3DBF19DC404 /* Pods-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-dummy.m"; sourceTree = ""; }; 2D326C0E925F5D55D604E945A6B1FF33 /* libLoremIpsum.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libLoremIpsum.a; sourceTree = BUILT_PRODUCTS_DIR; }; 37DB56D75062CC75FCB0966E1C6E8A8E /* Pods-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-acknowledgements.markdown"; sourceTree = ""; }; 38F8DCFB48A21AEC554454EBCAA8F121 /* UIResponder+SLKAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "UIResponder+SLKAdditions.h"; sourceTree = ""; }; 3E4E89230EF59BC255123B67864ACF77 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; 3FEDBEE9200697C2E3510ACDDC12CCB7 /* LoremIpsum.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = LoremIpsum.xcconfig; sourceTree = ""; }; 4CADA432C548A9883246BF013F60DAA5 /* SLKTextInputbar.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SLKTextInputbar.h; sourceTree = ""; }; 4E762F23EC34ED4A6FF3312D84E33A40 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Pods.debug.xcconfig; sourceTree = ""; }; 665F6403679EDB9C5E568C75F3FDF768 /* SLKTextView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = SLKTextView.m; sourceTree = ""; }; 6911BECA35E7518D864239B7E898EEF3 /* Pods-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-frameworks.sh"; sourceTree = ""; }; 904A23980152CBAEEEF9D965427462DD /* UIView+SLKAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "UIView+SLKAdditions.m"; sourceTree = ""; }; 90C65F0C6E8456B1AEA0D0C5FEAE0AB0 /* SLKTextView+SLKAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SLKTextView+SLKAdditions.m"; sourceTree = ""; }; 95ACD1202D9C5BA5ED706909142811B6 /* SLKTextView+SLKAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SLKTextView+SLKAdditions.h"; sourceTree = ""; }; 97317A6B5A0F446FE037FF688D606E96 /* libSlackTextViewController.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSlackTextViewController.a; sourceTree = BUILT_PRODUCTS_DIR; }; 98C98CDFB3F20F2925F6CD1F141BB14F /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Pods.release.xcconfig; sourceTree = ""; }; 9A3ABBD17931BCBDF6878F4179630D30 /* LoremIpsum-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "LoremIpsum-dummy.m"; sourceTree = ""; }; A1A36D34413696BE466E2CA0AFF194DA /* Pods-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-resources.sh"; sourceTree = ""; }; A2CC2D1A4404DE89733F4F6A85259C6B /* SLKTextView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SLKTextView.h; sourceTree = ""; }; B864757FAA2A46B94C852923F3AD40E5 /* SLKTextInput.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SLKTextInput.h; sourceTree = ""; }; BA32997CF782E3226A3B0F68E1D67789 /* SLKTextInput+Implementation.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SLKTextInput+Implementation.m"; sourceTree = ""; }; BA6428E9F66FD5A23C0A2E06ED26CD2F /* Podfile */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; C31B9359538DB1818FDCE030CB4C1695 /* SLKTextViewController.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SLKTextViewController.h; sourceTree = ""; }; C41834960415015B9F61AFB584936219 /* UIScrollView+SLKAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+SLKAdditions.m"; sourceTree = ""; }; C8AA8BA4CD934D5702F4394F583323E3 /* UIView+SLKAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "UIView+SLKAdditions.h"; sourceTree = ""; }; CD8D84605004C6D99DA69A6C6409F20F /* SLKUIConstants.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SLKUIConstants.h; sourceTree = ""; }; D8BC2A9F3D2387D410A5C3C03BDA3E17 /* LoremIpsum.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = LoremIpsum.h; path = LoremIpsum/LoremIpsum.h; sourceTree = ""; }; E1701D9CF721EA659038FE5E656FBAC3 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; E76EC386EBA8E2BD0919F33C32DBFB41 /* SLKTypingIndicatorView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SLKTypingIndicatorView.h; sourceTree = ""; }; EC2BA9C4B342C46DFB060C7EDAC0D02E /* SLKTypingIndicatorView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = SLKTypingIndicatorView.m; sourceTree = ""; }; EF0E8EF6D064E81BCCF9223C6D85EBD9 /* SLKTextInputbar.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = SLKTextInputbar.m; sourceTree = ""; }; F111576323FD30FF3762EBEA688A4BA5 /* SLKInputAccessoryView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = SLKInputAccessoryView.m; sourceTree = ""; }; F699546DA9ABB811511FC742AB1617EC /* SLKTextViewController.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = SLKTextViewController.m; sourceTree = ""; }; F99E54F177E07158903728CFCFC03D4B /* LoremIpsum.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = LoremIpsum.m; path = LoremIpsum/LoremIpsum.m; sourceTree = ""; }; F9C31BD51BFAD6262FB93C52997FD638 /* LoremIpsum-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "LoremIpsum-prefix.pch"; sourceTree = ""; }; FD3CEB520ABAE633DB696CD533769677 /* UIResponder+SLKAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "UIResponder+SLKAdditions.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 61B11975CF25C0B51D4F60785E661FF5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 9C4AC3CE673950588D03A76D94CFF5D1 /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; CABB99421975C1A01D769312A961D249 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 2A6186F8AE6C5CD2767949046F017961 /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; EF179D1F05656095DA02EEF384501F4B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F7AF382CA9A4A0680835E655B2210CEF /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 037C0CA694176A3C0915F62C9D20B3E6 /* Targets Support Files */ = { isa = PBXGroup; children = ( B3D1D13E0C6553800746CB8FD61CF946 /* Pods */, ); name = "Targets Support Files"; sourceTree = ""; }; 204A388122C8E209C1DD6446B97C35B9 /* Pods */ = { isa = PBXGroup; children = ( 86640DC7B8BB20512643103062A25BA9 /* LoremIpsum */, ); name = Pods; sourceTree = ""; }; 2BD6E7C0EABCAAB4CE8C1C81EFF50EC1 /* Support Files */ = { isa = PBXGroup; children = ( 3FEDBEE9200697C2E3510ACDDC12CCB7 /* LoremIpsum.xcconfig */, 9A3ABBD17931BCBDF6878F4179630D30 /* LoremIpsum-dummy.m */, F9C31BD51BFAD6262FB93C52997FD638 /* LoremIpsum-prefix.pch */, ); name = "Support Files"; path = "../Target Support Files/LoremIpsum"; sourceTree = ""; }; 62EF406D0F5E51578A82157FE682DD29 /* SlackTextViewController */ = { isa = PBXGroup; children = ( A384FD1284EAEA759553E32034DDB4CD /* Source */, E7D0A6299CC7408D1B7CAE837C31EF17 /* Support Files */, ); name = SlackTextViewController; path = ../..; sourceTree = ""; }; 7DB346D0F39D3F0E887471402A8071AB = { isa = PBXGroup; children = ( BA6428E9F66FD5A23C0A2E06ED26CD2F /* Podfile */, B95BFD2DB3C361D1161C3D56E5FF751E /* Development Pods */, BC3CA7F9E30CC8F7E2DD044DD34432FC /* Frameworks */, 204A388122C8E209C1DD6446B97C35B9 /* Pods */, 7E5D5C6516E714B975002980913A805B /* Products */, 037C0CA694176A3C0915F62C9D20B3E6 /* Targets Support Files */, ); sourceTree = ""; }; 7E5D5C6516E714B975002980913A805B /* Products */ = { isa = PBXGroup; children = ( 2D326C0E925F5D55D604E945A6B1FF33 /* libLoremIpsum.a */, E1701D9CF721EA659038FE5E656FBAC3 /* libPods.a */, 97317A6B5A0F446FE037FF688D606E96 /* libSlackTextViewController.a */, ); name = Products; sourceTree = ""; }; 86640DC7B8BB20512643103062A25BA9 /* LoremIpsum */ = { isa = PBXGroup; children = ( D8BC2A9F3D2387D410A5C3C03BDA3E17 /* LoremIpsum.h */, F99E54F177E07158903728CFCFC03D4B /* LoremIpsum.m */, 2BD6E7C0EABCAAB4CE8C1C81EFF50EC1 /* Support Files */, ); path = LoremIpsum; sourceTree = ""; }; A384FD1284EAEA759553E32034DDB4CD /* Source */ = { isa = PBXGroup; children = ( 0B0ECE21021FD69590E063D96AF6304E /* SLKInputAccessoryView.h */, F111576323FD30FF3762EBEA688A4BA5 /* SLKInputAccessoryView.m */, B864757FAA2A46B94C852923F3AD40E5 /* SLKTextInput.h */, BA32997CF782E3226A3B0F68E1D67789 /* SLKTextInput+Implementation.m */, 4CADA432C548A9883246BF013F60DAA5 /* SLKTextInputbar.h */, EF0E8EF6D064E81BCCF9223C6D85EBD9 /* SLKTextInputbar.m */, A2CC2D1A4404DE89733F4F6A85259C6B /* SLKTextView.h */, 665F6403679EDB9C5E568C75F3FDF768 /* SLKTextView.m */, 95ACD1202D9C5BA5ED706909142811B6 /* SLKTextView+SLKAdditions.h */, 90C65F0C6E8456B1AEA0D0C5FEAE0AB0 /* SLKTextView+SLKAdditions.m */, C31B9359538DB1818FDCE030CB4C1695 /* SLKTextViewController.h */, F699546DA9ABB811511FC742AB1617EC /* SLKTextViewController.m */, 1C71FEF7A4C22EEA18BBE9CBC9BE83BD /* SLKTypingIndicatorProtocol.h */, E76EC386EBA8E2BD0919F33C32DBFB41 /* SLKTypingIndicatorView.h */, EC2BA9C4B342C46DFB060C7EDAC0D02E /* SLKTypingIndicatorView.m */, CD8D84605004C6D99DA69A6C6409F20F /* SLKUIConstants.h */, 38F8DCFB48A21AEC554454EBCAA8F121 /* UIResponder+SLKAdditions.h */, FD3CEB520ABAE633DB696CD533769677 /* UIResponder+SLKAdditions.m */, 2544A16A1FD82EAC6B803C18D54A3F79 /* UIScrollView+SLKAdditions.h */, C41834960415015B9F61AFB584936219 /* UIScrollView+SLKAdditions.m */, C8AA8BA4CD934D5702F4394F583323E3 /* UIView+SLKAdditions.h */, 904A23980152CBAEEEF9D965427462DD /* UIView+SLKAdditions.m */, ); path = Source; sourceTree = ""; }; B3D1D13E0C6553800746CB8FD61CF946 /* Pods */ = { isa = PBXGroup; children = ( 37DB56D75062CC75FCB0966E1C6E8A8E /* Pods-acknowledgements.markdown */, 10834806BD7B412BC24F347361FA2C8E /* Pods-acknowledgements.plist */, 272643F56613CA0D336AE3DBF19DC404 /* Pods-dummy.m */, 6911BECA35E7518D864239B7E898EEF3 /* Pods-frameworks.sh */, A1A36D34413696BE466E2CA0AFF194DA /* Pods-resources.sh */, 4E762F23EC34ED4A6FF3312D84E33A40 /* Pods.debug.xcconfig */, 98C98CDFB3F20F2925F6CD1F141BB14F /* Pods.release.xcconfig */, ); name = Pods; path = "Target Support Files/Pods"; sourceTree = ""; }; B95BFD2DB3C361D1161C3D56E5FF751E /* Development Pods */ = { isa = PBXGroup; children = ( 62EF406D0F5E51578A82157FE682DD29 /* SlackTextViewController */, ); name = "Development Pods"; sourceTree = ""; }; BC3CA7F9E30CC8F7E2DD044DD34432FC /* Frameworks */ = { isa = PBXGroup; children = ( BF6342C8B29F4CEEA088EFF7AB4DE362 /* iOS */, ); name = Frameworks; sourceTree = ""; }; BF6342C8B29F4CEEA088EFF7AB4DE362 /* iOS */ = { isa = PBXGroup; children = ( 3E4E89230EF59BC255123B67864ACF77 /* Foundation.framework */, ); name = iOS; sourceTree = ""; }; E7D0A6299CC7408D1B7CAE837C31EF17 /* Support Files */ = { isa = PBXGroup; children = ( 21E917D789329016EA29D4FC962E79A2 /* SlackTextViewController.xcconfig */, 1BD4C1C4F8E3B6387290B1063443579F /* SlackTextViewController-dummy.m */, 1640D683B55420448C7F631CC6AC452B /* SlackTextViewController-prefix.pch */, ); name = "Support Files"; path = "Examples/Pods/Target Support Files/SlackTextViewController"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 38BB57B6A81A7CF758F60C8DA95BE94C /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 5541B1417BA993425E1B015992908C54 /* LoremIpsum.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 5459D226D55A63B053B7D6BD49106995 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( A4C9630F12E4A6AD3E9F415BE6E928B0 /* SLKInputAccessoryView.h in Headers */, CBB76607675EDA4E780C54D98CC5DF8F /* SLKTextInput.h in Headers */, 97942BFE0D2C0ACA0A69DEC48BC044B8 /* SLKTextInputbar.h in Headers */, 6C4AEEA57727B3B3D79FEBD64B156BF7 /* SLKTextView+SLKAdditions.h in Headers */, C875A458E52C3F05A2726D025A6CAA43 /* SLKTextView.h in Headers */, 4D4CD125276CAEF26B7E6433C1E9A458 /* SLKTextViewController.h in Headers */, FC26476E2D24910A0563D722B9BF5991 /* SLKTypingIndicatorProtocol.h in Headers */, 1D87BAF6807A6F24656CE3C6E0A3D691 /* SLKTypingIndicatorView.h in Headers */, 80D67571497E1D056B9AC4736F1C54E5 /* SLKUIConstants.h in Headers */, C2041517A4BA4A60D02C63E65449815D /* UIResponder+SLKAdditions.h in Headers */, 98F3BE534856D08CA2E0FC26CEE8AC1E /* UIScrollView+SLKAdditions.h in Headers */, CC8055ECB365838B897F70FC339A830C /* UIView+SLKAdditions.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ 2910582A42794C102B570588C150231A /* Pods */ = { isa = PBXNativeTarget; buildConfigurationList = DB2DD98C0D1248065C8282C08B87A1E1 /* Build configuration list for PBXNativeTarget "Pods" */; buildPhases = ( AFF94861E29A60B7887E0A19FFCAC76A /* Sources */, CABB99421975C1A01D769312A961D249 /* Frameworks */, ); buildRules = ( ); dependencies = ( 278C5FE308ACBD587D388D7B17743905 /* PBXTargetDependency */, 2EAEF0BC9F963272D9D9B28F37367879 /* PBXTargetDependency */, ); name = Pods; productName = Pods; productReference = E1701D9CF721EA659038FE5E656FBAC3 /* libPods.a */; productType = "com.apple.product-type.library.static"; }; B564AAE60BD4BB94F31E3C2A6F64164D /* SlackTextViewController */ = { isa = PBXNativeTarget; buildConfigurationList = 47BB6EBD726BE7B65251FB44D919CA1E /* Build configuration list for PBXNativeTarget "SlackTextViewController" */; buildPhases = ( 67AE67E0FE85DA58B62AE8650EF041A1 /* Sources */, 61B11975CF25C0B51D4F60785E661FF5 /* Frameworks */, 5459D226D55A63B053B7D6BD49106995 /* Headers */, ); buildRules = ( ); dependencies = ( ); name = SlackTextViewController; productName = SlackTextViewController; productReference = 97317A6B5A0F446FE037FF688D606E96 /* libSlackTextViewController.a */; productType = "com.apple.product-type.library.static"; }; D302A4C45741884C0F17816784845B9A /* LoremIpsum */ = { isa = PBXNativeTarget; buildConfigurationList = 49D43A79DA20627D2887901E4BA041AF /* Build configuration list for PBXNativeTarget "LoremIpsum" */; buildPhases = ( 25E717745441D4C345592521168F1663 /* Sources */, EF179D1F05656095DA02EEF384501F4B /* Frameworks */, 38BB57B6A81A7CF758F60C8DA95BE94C /* Headers */, ); buildRules = ( ); dependencies = ( ); name = LoremIpsum; productName = LoremIpsum; productReference = 2D326C0E925F5D55D604E945A6B1FF33 /* libLoremIpsum.a */; productType = "com.apple.product-type.library.static"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ D41D8CD98F00B204E9800998ECF8427E /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0700; LastUpgradeCheck = 0900; }; buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, ); mainGroup = 7DB346D0F39D3F0E887471402A8071AB; productRefGroup = 7E5D5C6516E714B975002980913A805B /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( D302A4C45741884C0F17816784845B9A /* LoremIpsum */, 2910582A42794C102B570588C150231A /* Pods */, B564AAE60BD4BB94F31E3C2A6F64164D /* SlackTextViewController */, ); }; /* End PBXProject section */ /* Begin PBXSourcesBuildPhase section */ 25E717745441D4C345592521168F1663 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( BB5DE846FF718133D67729717C4C6CC9 /* LoremIpsum-dummy.m in Sources */, 89D3F183FA5B8BA60E82030FBA24D1E1 /* LoremIpsum.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 67AE67E0FE85DA58B62AE8650EF041A1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 1B54941E5E719905EA0976C1582AA362 /* SlackTextViewController-dummy.m in Sources */, 9303F6EC3D95E616D80AE7C30737487A /* SLKInputAccessoryView.m in Sources */, F76DD4435CA2C94F6E441704E37D9E24 /* SLKTextInput+Implementation.m in Sources */, 569E350F767E9D5BC2A2D44B5F629336 /* SLKTextInputbar.m in Sources */, 7DB3F03605631A6742020ADBD96EE73A /* SLKTextView+SLKAdditions.m in Sources */, EF0353E1B59D852A952B7AE54AB72200 /* SLKTextView.m in Sources */, AA5A83ABE4694129065A4F45DBAEAC97 /* SLKTextViewController.m in Sources */, 52855809CE7672258D6707D8EB1DA870 /* SLKTypingIndicatorView.m in Sources */, 3B67741D6C7B5D4C5B8AFCC351C7F424 /* UIResponder+SLKAdditions.m in Sources */, DA9CF5D6ED28E84CC33F5EFF2D97F915 /* UIScrollView+SLKAdditions.m in Sources */, E9015CEB577B281532777C0D97D80FA1 /* UIView+SLKAdditions.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; AFF94861E29A60B7887E0A19FFCAC76A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 62F2C6DA240FB3A9602CA0046869AAF2 /* Pods-dummy.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 278C5FE308ACBD587D388D7B17743905 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = LoremIpsum; target = D302A4C45741884C0F17816784845B9A /* LoremIpsum */; targetProxy = 63B962AF9C8829F370F8AA092B98A969 /* PBXContainerItemProxy */; }; 2EAEF0BC9F963272D9D9B28F37367879 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = SlackTextViewController; target = B564AAE60BD4BB94F31E3C2A6F64164D /* SlackTextViewController */; targetProxy = 7396AD0C520CB4738B2901477D4495E1 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 052A17875CB827423D627183396CEB60 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = "RELEASE=1"; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; STRIP_INSTALLED_PRODUCT = NO; SYMROOT = "${SRCROOT}/../build"; VALIDATE_PRODUCT = YES; }; name = Release; }; 20874F4DD8D0A9E3236782DA6F09DDBA /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 98C98CDFB3F20F2925F6CD1F141BB14F /* Pods.release.xcconfig */; buildSettings = { ENABLE_STRICT_OBJC_MSGSEND = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MACH_O_TYPE = staticlib; MTL_ENABLE_DEBUG_INFO = NO; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PODS_ROOT = "$(SRCROOT)"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; }; name = Release; }; 40045C1ED2FAF3AAE21E26272143B92F /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 21E917D789329016EA29D4FC962E79A2 /* SlackTextViewController.xcconfig */; buildSettings = { ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_PREFIX_HEADER = "Target Support Files/SlackTextViewController/SlackTextViewController-prefix.pch"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PRIVATE_HEADERS_FOLDER_PATH = ""; PRODUCT_NAME = "$(TARGET_NAME)"; PUBLIC_HEADERS_FOLDER_PATH = ""; SDKROOT = iphoneos; SKIP_INSTALL = YES; }; name = Release; }; 981D5D11BA9B33F42310BE41E82BE285 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4E762F23EC34ED4A6FF3312D84E33A40 /* Pods.debug.xcconfig */; buildSettings = { ENABLE_STRICT_OBJC_MSGSEND = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MACH_O_TYPE = staticlib; MTL_ENABLE_DEBUG_INFO = YES; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PODS_ROOT = "$(SRCROOT)"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; }; name = Debug; }; B37F0F91F85060E28F1DAAB522DC7EC1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; ONLY_ACTIVE_ARCH = YES; STRIP_INSTALLED_PRODUCT = NO; SYMROOT = "${SRCROOT}/../build"; }; name = Debug; }; E2F333771CD1D6BCCA4691191B77DDCA /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 21E917D789329016EA29D4FC962E79A2 /* SlackTextViewController.xcconfig */; buildSettings = { ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_PREFIX_HEADER = "Target Support Files/SlackTextViewController/SlackTextViewController-prefix.pch"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PRIVATE_HEADERS_FOLDER_PATH = ""; PRODUCT_NAME = "$(TARGET_NAME)"; PUBLIC_HEADERS_FOLDER_PATH = ""; SDKROOT = iphoneos; SKIP_INSTALL = YES; }; name = Debug; }; EAD2C2C0247FDFB4CBCA4D275C77C3AF /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 3FEDBEE9200697C2E3510ACDDC12CCB7 /* LoremIpsum.xcconfig */; buildSettings = { ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_PREFIX_HEADER = "Target Support Files/LoremIpsum/LoremIpsum-prefix.pch"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PRIVATE_HEADERS_FOLDER_PATH = ""; PRODUCT_NAME = "$(TARGET_NAME)"; PUBLIC_HEADERS_FOLDER_PATH = ""; SDKROOT = iphoneos; SKIP_INSTALL = YES; }; name = Release; }; F3160581E37B5221322D2648ACD18C99 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 3FEDBEE9200697C2E3510ACDDC12CCB7 /* LoremIpsum.xcconfig */; buildSettings = { ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_PREFIX_HEADER = "Target Support Files/LoremIpsum/LoremIpsum-prefix.pch"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PRIVATE_HEADERS_FOLDER_PATH = ""; PRODUCT_NAME = "$(TARGET_NAME)"; PUBLIC_HEADERS_FOLDER_PATH = ""; SDKROOT = iphoneos; SKIP_INSTALL = YES; }; name = Debug; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */ = { isa = XCConfigurationList; buildConfigurations = ( B37F0F91F85060E28F1DAAB522DC7EC1 /* Debug */, 052A17875CB827423D627183396CEB60 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 47BB6EBD726BE7B65251FB44D919CA1E /* Build configuration list for PBXNativeTarget "SlackTextViewController" */ = { isa = XCConfigurationList; buildConfigurations = ( E2F333771CD1D6BCCA4691191B77DDCA /* Debug */, 40045C1ED2FAF3AAE21E26272143B92F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 49D43A79DA20627D2887901E4BA041AF /* Build configuration list for PBXNativeTarget "LoremIpsum" */ = { isa = XCConfigurationList; buildConfigurations = ( F3160581E37B5221322D2648ACD18C99 /* Debug */, EAD2C2C0247FDFB4CBCA4D275C77C3AF /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; DB2DD98C0D1248065C8282C08B87A1E1 /* Build configuration list for PBXNativeTarget "Pods" */ = { isa = XCConfigurationList; buildConfigurations = ( 981D5D11BA9B33F42310BE41E82BE285 /* Debug */, 20874F4DD8D0A9E3236782DA6F09DDBA /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = D41D8CD98F00B204E9800998ECF8427E /* Project object */; } ================================================ FILE: Examples/Pods/Target Support Files/LoremIpsum/LoremIpsum-dummy.m ================================================ #import @interface PodsDummy_LoremIpsum : NSObject @end @implementation PodsDummy_LoremIpsum @end ================================================ FILE: Examples/Pods/Target Support Files/LoremIpsum/LoremIpsum-prefix.pch ================================================ #ifdef __OBJC__ #import #endif ================================================ FILE: Examples/Pods/Target Support Files/LoremIpsum/LoremIpsum.xcconfig ================================================ GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Private/LoremIpsum" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/LoremIpsum" "${PODS_ROOT}/Headers/Public/SlackTextViewController" PODS_ROOT = ${SRCROOT} SKIP_INSTALL = YES ================================================ FILE: Examples/Pods/Target Support Files/Pods/Pods-acknowledgements.markdown ================================================ # Acknowledgements This application makes use of the following third party libraries: ## LoremIpsum Copyright (c) 2013 Lukas Kubanek. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## SlackTextViewController Copyright (c) Slack Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Generated by CocoaPods - http://cocoapods.org ================================================ FILE: Examples/Pods/Target Support Files/Pods/Pods-acknowledgements.plist ================================================ PreferenceSpecifiers FooterText This application makes use of the following third party libraries: Title Acknowledgements Type PSGroupSpecifier FooterText Copyright (c) 2013 Lukas Kubanek. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Title LoremIpsum Type PSGroupSpecifier FooterText Copyright (c) Slack Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Title SlackTextViewController Type PSGroupSpecifier FooterText Generated by CocoaPods - http://cocoapods.org Title Type PSGroupSpecifier StringsTable Acknowledgements Title Acknowledgements ================================================ FILE: Examples/Pods/Target Support Files/Pods/Pods-dummy.m ================================================ #import @interface PodsDummy_Pods : NSObject @end @implementation PodsDummy_Pods @end ================================================ FILE: Examples/Pods/Target Support Files/Pods/Pods-frameworks.sh ================================================ #!/bin/sh set -e echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" install_framework() { if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then local source="${BUILT_PRODUCTS_DIR}/$1" elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" elif [ -r "$1" ]; then local source="$1" fi local destination="${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" if [ -L "${source}" ]; then echo "Symlinked..." source="$(readlink "${source}")" fi # use filter instead of exclude so missing patterns dont' throw errors echo "rsync -av --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" rsync -av --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" local basename basename="$(basename -s .framework "$1")" binary="${destination}/${basename}.framework/${basename}" if ! [ -r "$binary" ]; then binary="${destination}/${basename}" fi # Strip invalid architectures so "fat" simulator / device frameworks work on device if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then strip_invalid_archs "$binary" fi # Resign the code if required by the build settings to avoid unstable apps code_sign_if_enabled "${destination}/$(basename "$1")" # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then local swift_runtime_libs swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) for lib in $swift_runtime_libs; do echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" code_sign_if_enabled "${destination}/${lib}" done fi } # Signs a framework with the provided identity code_sign_if_enabled() { if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then # Use the current code_sign_identitiy echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" echo "/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --preserve-metadata=identifier,entitlements \"$1\"" /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --preserve-metadata=identifier,entitlements "$1" fi } # Strip invalid architectures strip_invalid_archs() { binary="$1" # Get architectures for current file archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | rev)" stripped="" for arch in $archs; do if ! [[ "${VALID_ARCHS}" == *"$arch"* ]]; then # Strip non-valid architectures in-place lipo -remove "$arch" -output "$binary" "$binary" || exit 1 stripped="$stripped $arch" fi done if [[ "$stripped" ]]; then echo "Stripped $binary of architectures:$stripped" fi } ================================================ FILE: Examples/Pods/Target Support Files/Pods/Pods-resources.sh ================================================ #!/bin/sh set -e mkdir -p "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt > "$RESOURCES_TO_COPY" XCASSET_FILES=() realpath() { DIRECTORY="$(cd "${1%/*}" && pwd)" FILENAME="${1##*/}" echo "$DIRECTORY/$FILENAME" } install_resource() { case $1 in *.storyboard) echo "ibtool --reference-external-strings-file --errors --warnings --notices --output-format human-readable-text --compile ${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$1\" .storyboard`.storyboardc ${PODS_ROOT}/$1 --sdk ${SDKROOT}" ibtool --reference-external-strings-file --errors --warnings --notices --output-format human-readable-text --compile "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$1\" .storyboard`.storyboardc" "${PODS_ROOT}/$1" --sdk "${SDKROOT}" ;; *.xib) echo "ibtool --reference-external-strings-file --errors --warnings --notices --output-format human-readable-text --compile ${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$1\" .xib`.nib ${PODS_ROOT}/$1 --sdk ${SDKROOT}" ibtool --reference-external-strings-file --errors --warnings --notices --output-format human-readable-text --compile "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$1\" .xib`.nib" "${PODS_ROOT}/$1" --sdk "${SDKROOT}" ;; *.framework) echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" echo "rsync -av ${PODS_ROOT}/$1 ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" rsync -av "${PODS_ROOT}/$1" "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" ;; *.xcdatamodel) echo "xcrun momc \"${PODS_ROOT}/$1\" \"${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$1"`.mom\"" xcrun momc "${PODS_ROOT}/$1" "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$1" .xcdatamodel`.mom" ;; *.xcdatamodeld) echo "xcrun momc \"${PODS_ROOT}/$1\" \"${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$1" .xcdatamodeld`.momd\"" xcrun momc "${PODS_ROOT}/$1" "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$1" .xcdatamodeld`.momd" ;; *.xcmappingmodel) echo "xcrun mapc \"${PODS_ROOT}/$1\" \"${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$1" .xcmappingmodel`.cdm\"" xcrun mapc "${PODS_ROOT}/$1" "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$1" .xcmappingmodel`.cdm" ;; *.xcassets) ABSOLUTE_XCASSET_FILE=$(realpath "${PODS_ROOT}/$1") XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") ;; /*) echo "$1" echo "$1" >> "$RESOURCES_TO_COPY" ;; *) echo "${PODS_ROOT}/$1" echo "${PODS_ROOT}/$1" >> "$RESOURCES_TO_COPY" ;; esac } mkdir -p "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" fi rm -f "$RESOURCES_TO_COPY" if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] then case "${TARGETED_DEVICE_FAMILY}" in 1,2) TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" ;; 1) TARGET_DEVICE_ARGS="--target-device iphone" ;; 2) TARGET_DEVICE_ARGS="--target-device ipad" ;; *) TARGET_DEVICE_ARGS="--target-device mac" ;; esac # Find all other xcassets (this unfortunately includes those of path pods and other targets). OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) while read line; do if [[ $line != "`realpath $PODS_ROOT`*" ]]; then XCASSET_FILES+=("$line") fi done <<<"$OTHER_XCASSETS" printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${IPHONEOS_DEPLOYMENT_TARGET}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" fi ================================================ FILE: Examples/Pods/Target Support Files/Pods/Pods.debug.xcconfig ================================================ GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/LoremIpsum" "${PODS_ROOT}/Headers/Public/SlackTextViewController" OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/LoremIpsum" -isystem "${PODS_ROOT}/Headers/Public/SlackTextViewController" OTHER_LDFLAGS = $(inherited) -ObjC -l"LoremIpsum" -l"SlackTextViewController" PODS_ROOT = ${SRCROOT}/Pods ================================================ FILE: Examples/Pods/Target Support Files/Pods/Pods.release.xcconfig ================================================ GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/LoremIpsum" "${PODS_ROOT}/Headers/Public/SlackTextViewController" OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/LoremIpsum" -isystem "${PODS_ROOT}/Headers/Public/SlackTextViewController" OTHER_LDFLAGS = $(inherited) -ObjC -l"LoremIpsum" -l"SlackTextViewController" PODS_ROOT = ${SRCROOT}/Pods ================================================ FILE: Examples/Pods/Target Support Files/SlackTextViewController/SlackTextViewController-dummy.m ================================================ #import @interface PodsDummy_SlackTextViewController : NSObject @end @implementation PodsDummy_SlackTextViewController @end ================================================ FILE: Examples/Pods/Target Support Files/SlackTextViewController/SlackTextViewController-prefix.pch ================================================ #ifdef __OBJC__ #import #endif ================================================ FILE: Examples/Pods/Target Support Files/SlackTextViewController/SlackTextViewController.xcconfig ================================================ GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Private/SlackTextViewController" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/LoremIpsum" "${PODS_ROOT}/Headers/Public/SlackTextViewController" PODS_ROOT = ${SRCROOT} SKIP_INSTALL = YES ================================================ FILE: File Templates/SlackTextView Controller.xctemplate/TemplateInfo.plist ================================================ AllowedTypes public.objective-c-source public.objective-c-plus-plus-source Platforms com.apple.platform.iphoneos Description SlackTextViewController subclass file DefaultCompletionName MyTextViewController Kind Xcode.IDEKit.TextSubstitutionFileTemplateKind Summary A SLKTextViewController subclass MainTemplateFile ___FILEBASENAME___.m SortOrder 1 Options Description The name of the class to create Identifier productName Name Class: NotPersisted Required Type text Default SLKTextViewController Description The default parent class SLKTextViewController Identifier textViewControllerSubclass Name Subclass of: Required Type static Default UITableView Description The content view type Identifier contentViewChoice Name Content View: Required Type popup Values UITableView UICollectionView ================================================ FILE: File Templates/SlackTextView Controller.xctemplate/UICollectionView/___FILEBASENAME___.h ================================================ // // ___FILENAME___ // ___PROJECTNAME___ // // Created by ___FULLUSERNAME___ on ___DATE___. //___COPYRIGHT___ // #import "SLKTextViewController.h" @interface ___FILEBASENAMEASIDENTIFIER___ : SLKTextViewController @end ================================================ FILE: File Templates/SlackTextView Controller.xctemplate/UICollectionView/___FILEBASENAME___.m ================================================ // // ___FILENAME___ // ___PROJECTNAME___ // // Created by ___FULLUSERNAME___ on ___DATE___. //___COPYRIGHT___ // #import "___FILEBASENAME___.h" @interface ___FILEBASENAMEASIDENTIFIER___ () @end @implementation ___FILEBASENAMEASIDENTIFIER___ #pragma mark - Initializer - (id)init { #warning Potentially incomplete method implementation. self = [super initWithCollectionViewLayout:<#(UICollectionViewLayout *)#>]; if (self) { } return self; } /* // Uncomment if you are using Storyboard. // You don't need to call initWithCoder: anymore + (UICollectionViewLayout *)collectionViewLayoutForCoder:(NSCoder *)decoder { return <#(UICollectionViewLayout *)#>; } */ #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Do view setup here. } #pragma mark - SLKTextViewController Events - (void)didChangeKeyboardStatus:(SLKKeyboardStatus)status { // Notifies the view controller that the keyboard changed status. // Calling super does nothing } - (void)textWillUpdate { // Notifies the view controller that the text will update. // Calling super does nothing [super textWillUpdate]; } - (void)textDidUpdate:(BOOL)animated { // Notifies the view controller that the text did update. // Must call super [super textDidUpdate:animated]; } - (BOOL)canPressRightButton { // Asks if the right button can be pressed return [super canPressRightButton]; } - (void)didPressRightButton:(id)sender { // Notifies the view controller when the right button's action has been triggered, manually or by using the keyboard return key. // Must call super // This little trick validates any pending auto-correction or auto-spelling just after hitting the 'Send' button [self.textView refreshFirstResponder]; [super didPressRightButton:sender]; } /* // Uncomment these methods for aditional events - (void)didPressLeftButton:(id)sender { // Notifies the view controller when the left button's action has been triggered, manually. [super didPressLeftButton:sender]; } - (id)keyForTextCaching { // Return any valid key object for enabling text caching while composing in the text view. // Calling super does nothing } - (void)didPasteMediaContent:(NSDictionary *)userInfo { // Notifies the view controller when a user did paste a media content inside of the text view // Calling super does nothing } - (void)willRequestUndo { // Notification about when a user did shake the device to undo the typed text [super willRequestUndo]; } */ #pragma mark - SLKTextViewController Edition /* // Uncomment these methods to enable edit mode - (void)didCommitTextEditing:(id)sender { // Notifies the view controller when tapped on the right "Accept" button for commiting the edited text [super didCommitTextEditing:sender]; } - (void)didCancelTextEditing:(id)sender { // Notifies the view controller when tapped on the left "Cancel" button [super didCancelTextEditing:sender]; } */ #pragma mark - SLKTextViewController Autocompletion /* // Uncomment these methods to enable autocompletion mode - (BOOL)canShowAutoCompletion { // Asks of the autocompletion view should be shown return NO; } - (CGFloat)heightForAutoCompletionView { // Asks for the height of the autocompletion view return 0.0; } */ #pragma mark - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { #warning Incomplete method implementation -- Return the number of sections return 0; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { #warning Incomplete method implementation -- Return the number of items in the section return 0; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:<#@"reuseIdentifier"#> forIndexPath:indexPath]; // Configure the cell return cell; } #pragma mark /* // Uncomment this method to specify if the specified item should be highlighted during tracking - (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath { return YES; } */ /* // Uncomment this method to specify if the specified item should be selected - (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath { return YES; } */ /* // Uncomment these methods to specify if an action menu should be displayed for the specified item, and react to actions performed on the item - (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath { return NO; } - (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { return NO; } - (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { } */ #pragma mark - - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { #warning Potentially incomplete method implementation. // Returns the number of sections. return 0; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { #warning Incomplete method implementation. // Returns the number of rows in the section. if ([tableView isEqual:self.autoCompletionView]) { return 0; } return 0; } /* // Uncomment these methods to configure the cells - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:<#@"reuseIdentifier"#> forIndexPath:indexPath]; if ([tableView isEqual:self.autoCompletionView]) { // Configure the autocompletion cell... } return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { // Returns the height each row if ([tableView isEqual:self.autoCompletionView]) { return 0; } } */ #pragma mark - /* // Uncomment this method to handle the cell selection - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if ([tableView isEqual:self.autoCompletionView]) { [self acceptAutoCompletionWithString:<#@"any_string"#>]; } } */ #pragma mark - View lifeterm - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)dealloc { } @end ================================================ FILE: File Templates/SlackTextView Controller.xctemplate/UITableView/___FILEBASENAME___.h ================================================ // // ___FILENAME___ // ___PROJECTNAME___ // // Created by ___FULLUSERNAME___ on ___DATE___. //___COPYRIGHT___ // #import "SLKTextViewController.h" @interface ___FILEBASENAMEASIDENTIFIER___ : SLKTextViewController @end ================================================ FILE: File Templates/SlackTextView Controller.xctemplate/UITableView/___FILEBASENAME___.m ================================================ // // ___FILENAME___ // ___PROJECTNAME___ // // Created by ___FULLUSERNAME___ on ___DATE___. //___COPYRIGHT___ // #import "___FILEBASENAME___.h" @interface ___FILEBASENAMEASIDENTIFIER___ () @end @implementation ___FILEBASENAMEASIDENTIFIER___ #pragma mark - Initializer - (id)init { #warning Potentially incomplete method implementation. self = [super initWithTableViewStyle:<#(UITableViewStyle)#>]; if (self) { } return self; } /* // Uncomment if you are using Storyboard. // You don't need to call initWithCoder: anymore + (UITableViewStyle)tableViewStyleForCoder:(NSCoder *)decoder { return <#(UITableViewStyle)#>; } */ #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Do view setup here. } #pragma mark - SLKTextViewController Events - (void)didChangeKeyboardStatus:(SLKKeyboardStatus)status { // Notifies the view controller that the keyboard changed status. // Calling super does nothing } - (void)textWillUpdate { // Notifies the view controller that the text will update. // Calling super does nothing [super textWillUpdate]; } - (void)textDidUpdate:(BOOL)animated { // Notifies the view controller that the text did update. // Must call super [super textDidUpdate:animated]; } - (BOOL)canPressRightButton { // Asks if the right button can be pressed return [super canPressRightButton]; } - (void)didPressRightButton:(id)sender { // Notifies the view controller when the right button's action has been triggered, manually or by using the keyboard return key. // Must call super // This little trick validates any pending auto-correction or auto-spelling just after hitting the 'Send' button [self.textView refreshFirstResponder]; [super didPressRightButton:sender]; } /* // Uncomment these methods for aditional events - (void)didPressLeftButton:(id)sender { // Notifies the view controller when the left button's action has been triggered, manually. [super didPressLeftButton:sender]; } - (id)keyForTextCaching { // Return any valid key object for enabling text caching while composing in the text view. // Calling super does nothing } - (void)didPasteMediaContent:(NSDictionary *)userInfo { // Notifies the view controller when a user did paste a media content inside of the text view // Calling super does nothing } - (void)willRequestUndo { // Notification about when a user did shake the device to undo the typed text [super willRequestUndo]; } */ #pragma mark - SLKTextViewController Edition /* // Uncomment these methods to enable edit mode - (void)didCommitTextEditing:(id)sender { // Notifies the view controller when tapped on the right "Accept" button for commiting the edited text [super didCommitTextEditing:sender]; } - (void)didCancelTextEditing:(id)sender { // Notifies the view controller when tapped on the left "Cancel" button [super didCancelTextEditing:sender]; } */ #pragma mark - SLKTextViewController Autocompletion /* // Uncomment these methods to enable autocompletion mode - (BOOL)canShowAutoCompletion { // Asks of the autocompletion view should be shown return NO; } - (CGFloat)heightForAutoCompletionView { // Asks for the height of the autocompletion view return 0.0; } */ #pragma mark - - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { #warning Potentially incomplete method implementation. // Returns the number of sections. return 0; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { #warning Incomplete method implementation. // Returns the number of rows in the section. if ([tableView isEqual:self.autoCompletionView]) { return 0; } return 0; } /* // Uncomment these methods to configure the cells - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:<#@"reuseIdentifier"#> forIndexPath:indexPath]; if ([tableView isEqual:self.autoCompletionView]) { // Configure the autocompletion cell... } else if ([tableView isEqual:self.tableView]) { // Configure the message cell... } return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { // Returns the height each row if ([tableView isEqual:self.autoCompletionView]) { return 0; } return 0; } */ #pragma mark - /* // Uncomment this method to handle the cell selection - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if ([tableView isEqual:self.tableView]) { } if ([tableView isEqual:self.autoCompletionView]) { [self acceptAutoCompletionWithString:<#@"any_string"#>]; } } */ #pragma mark - View lifeterm - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)dealloc { } @end ================================================ FILE: File Templates/install.sh ================================================ #!/usr/bin/env bash # Default the folder name folderName="Slack Templates" # Determine the source directory. sourceDirectory=`dirname "$BASH_SOURCE"` # Determine the install directory. installDirectory=~/Library/Developer/Xcode/Templates/File\ Templates/"$folderName" echo "Templates will be installed to $installDirectory" # Delete the install directory if it already exists to prevent deleted files from lingering. if [ -d "$installDirectory" ] then rm -r "$installDirectory" fi # Create the install directory. mkdir -p "$installDirectory" # Copy all of the xctemplate folders into the install directory. cp -r "$sourceDirectory"/*.xctemplate "$installDirectory" ================================================ FILE: Gemfile ================================================ source 'http://rubygems.org' gem 'cocoapods', "~> 0.39" gem 'cocoapods-coverage', '~> 0.2' gem 'coveralls', require: false gem 'slather', '~> 1.8' gem 'xcpretty' gem 'nokogiri', '~> 1.6' ================================================ FILE: LICENSE ================================================ Copyright (c) Slack Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Deprecation We are no longer providing support for `SlackTextViewController`. This project satisfied all of our iOS messaging needs in the past and we are proud to have contributed it to the open-source community. Today, in order to delight our users with a solution that is highly tailored and rapidly iterated-upon, we have shifted focus to our internal projects. Unfortunately, this renders us lacking the capacity to support our past projects in addition to our newer, internal projects. This project has been deprecated as a result. # SlackTextViewController **IMPORTANT NOTICE: Please update to >= `1.9` to avoid any risk of app rejection. More details in [#361](https://github.com/slackhq/SlackTextViewController/issues/361)** [![License](http://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) [![Pod Version](https://img.shields.io/cocoapods/v/SlackTextViewController.svg)](http://cocoadocs.org/docsets/SlackTextViewController/1.9/) [![Carthage compatible](https://img.shields.io/badge/carthage-compatible-F5B369.svg)](https://github.com/Carthage/Carthage) [![BuddyBuild](https://dashboard.buddybuild.com/api/statusImage?appID=59f2234423c5f600018bb24b&branch=master&build=latest)](https://dashboard.buddybuild.com/apps/59f2234423c5f600018bb24b/build/latest?branch=master) A drop-in UIViewController subclass with a growing text input view and other useful messaging features. Meant to be a replacement for UITableViewController & UICollectionViewController. ![Demo Gif](Screenshots/slacktextviewcontroller_demo.gif) This library was historically used in our iOS app. At its inception, the library satisfied our product needs and was flexible enough to be reused by others wanting to build great messaging apps for iOS. ## Feature List ### Core - Works out of the box with [UITableView or UICollectionView or UIScrollView](https://github.com/slackhq/SlackTextViewController#subclassing) - [Growing Text View](https://github.com/slackhq/SlackTextViewController#growing-text-view), with line count limit support - Flexible UI built with Auto Layout - Customizable: provides left and right button, and toolbar outlets - Tap Gesture for dismissing the keyboard - [External keyboard](https://github.com/slackhq/SlackTextViewController#external-keyboard) commands support - Undo/Redo (with keyboard commands and UIMenuController) - Text Appending APIs ### Additional - [Autocomplete Mode](https://github.com/slackhq/SlackTextViewController#autocompletion) by registering any prefix key (`@`, `#`, `/`) - [Edit Mode](https://github.com/slackhq/SlackTextViewController#edit-mode) - [Markdown Formatting](https://github.com/slackhq/SlackTextViewController#markdown-formatting) - [Typing Indicator](https://github.com/slackhq/SlackTextViewController#typing-indicator) display - [Shake Gesture](https://github.com/slackhq/SlackTextViewController#shake-gesture) for clearing text view - Multimedia Pasting (png, gif, mov, etc.) - [Inverted Mode](https://github.com/slackhq/SlackTextViewController#inverted-mode) for displaying cells upside-down (using CATransform) -- a necessary hack for some messaging apps. `YES`/`true` by default, so beware, your entire cells might be flipped! - Tap Gesture for dismissing the keyboard - [Panning Gesture](https://github.com/slackhq/SlackTextViewController#panning-gesture) for sliding down/up the keyboard - [Hideable TextInputbar](https://github.com/slackhq/SlackTextViewController#hideable-textinputbar) - [Dynamic Type](https://github.com/slackhq/SlackTextViewController#dynamic-type) for adjusting automatically the text input bar height based on the font size. - Bouncy Animations ### Compatibility - Carthage & CocoaPods - Objective-C & Swift - iOS 7, 8 & 9 - iPhone & iPad - [Storyboard](https://github.com/slackhq/SlackTextViewController#storyboard) - UIPopOverController & UITabBarController - Container View Controller - Auto-Rotation - iPad Multitasking (iOS 9 only) - Localization ## Installation ###### With [CocoaPods](https://cocoapods.org/): ```ruby pod "SlackTextViewController" ``` ###### With [Carthage](https://github.com/Carthage/Carthage): ```swift github "slackhq/SlackTextViewController" ``` ###### Manually: There are two ways to do this: - Copy and drag the `Source/` folder to your project. - or compile the project located in `Builder/SlackTextViewController.xcodeproj` to create a `SlackTextViewController.framework` package. You could also [link the library into your project](https://developer.apple.com/library/ios/recipes/xcode_help-project_editor/Articles/AddingaLibrarytoaTarget.html#//apple_ref/doc/uid/TP40010155-CH17-SW1). ## How to use ### Subclassing `SLKTextViewController` is meant to be subclassed, like you would normally do with UITableViewController or UICollectionViewController or UIScrollView. This pattern is a convenient way of extending UIViewController. SlackTextViewController manages a lot behind the scenes while still providing the ability to add custom behaviours. You may override methods, and decide to call super and perform additional logic, or not to call super and override default logic. Start by creating a new subclass of `SLKTextViewController`. In the init overriding method, if you wish to use the `UITableView` version, call: ##### Obj-C ```objc [super initWithTableViewStyle:UITableViewStylePlain] ``` ##### Swift ```swift super.init(tableViewStyle: .Plain) ``` or the `UICollectionView` version: ##### Obj-C ```objc [super initWithCollectionViewLayout:[UICollectionViewFlowLayout new]] ``` ##### Swift ```swift super.init(collectionViewLayout: UICollectionViewFlowLayout()) ``` or the `UIScrollView` version: ##### Obj-C ```objc [super initWithScrollView:self.myStrongScrollView] ``` ##### Swift ```swift super.init(scrollView: self.myStrongScrollView) ``` Protocols like `UITableViewDelegate` and `UITableViewDataSource` are already setup for you. You will be able to call whatever delegate and data source methods you need for customising your control. Calling `[super init]` will call `[super initWithTableViewStyle:UITableViewStylePlain]` by default. ### Storyboard When using SlackTextViewController with storyboards, instead of overriding the traditional `initWithCoder:` you will need to override any of the two custom methods below. This approach helps preserving the exact same features from the programatic approach, but also limits the edition of the nib of your `SLKTextViewController` subclass since it doesn't layout subviews from the nib (subviews are still initialized and layed out programatically). if you wish to use the `UITableView` version, call: ##### Obj-C ```objc + (UITableViewStyle)tableViewStyleForCoder:(NSCoder *)decoder { return UITableViewStylePlain; } ``` ##### Swift ```swift override class func tableViewStyleForCoder(decoder: NSCoder) -> UITableViewStyle { return .Plain } ``` or the `UICollectionView` version: ##### Obj-C ```objc + (UICollectionViewLayout *)collectionViewLayoutForCoder:(NSCoder *)decoder { return [UICollectionViewFlowLayout new]; } ``` ##### Swift ```swift override class func collectionViewLayoutForCoder(decoder: NSCoder) -> UICollectionViewLayout { return UICollectionViewFlowLayout() } ``` ### Sample Project Check out the sample project, everything is demo'd there. There are 2 main examples (different targets) for testing the programatic and storyboard approaches, and a Swift example. Most of the features are implemented for you to quickly start using them. Feel free to contribute! ## Features ### Growing Text View ![Growing](Screenshots/screenshot_auto-expanding.png) The text view expands automatically when a new line is required, until it reaches its `maxNumberOfLines`value. You may change this property's value in the textView. By default, the number of lines is set to best fit each device dimensions: - iPhone 4 (<=480pts): 4 lines - iPhone 5/6 (>=568pts): 6 lines - iPad (>=768pts): 8 lines On iPhone devices, in landscape orientation, the maximum number of lines is changed to fit the available space. ### Inverted Mode Some layouts may require to show from bottom to top and new subviews are inserted from the bottom. To enable this, you must use the `inverted` flag property (default is `YES`/`true`). This will actually invert the entire ScrollView object. Make sure to apply the same transformation to every subview. In the case of UITableView, the best place for adjusting the transformation is in its data source methods like: ##### Obj-C ```objc - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:kCellIdentifier]; cell.transform = self.tableView.transform; } ``` ##### Swift ```swift override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { if let cell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) { cell.transform = self.tableView.transform } } ``` ### Autocompletion We use autocompletion for many things: names, channels, emoji, and more. ![Autocompletion](Screenshots/screenshot_auto-completion.png) To set up autocompletion in your app, follow these simple steps: #### 1. Registration You must first register all the prefixes you'd like to support for autocompletion detection: ##### Obj-C ```objc [self registerPrefixesForAutoCompletion:@[@"#"]]; ``` ##### Swift ```swift self.registerPrefixesForAutoCompletion(["@", "#"]) ``` #### 2. Processing Every time a new character is inserted in the text view, the nearest word to the caret will be processed and verified if it contains any of the registered prefixes. Once the prefix has been detected, `didChangeAutoCompletionPrefix:andWord:` will be called. This is the perfect place to populate your data source and show/hide the autocompletion view. So you must override it in your subclass, to be able to perform additional tasks. Default returns NO. ##### Obj-C ```objc - (void)didChangeAutoCompletionPrefix:(NSString *)prefix andWord:(NSString *)word { NSArray *array = [NSArray arrayWithArray:self.channels]; if ([prefix isEqualToString:@"#"] && word.length > 0) { self.searchResult = [array filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self BEGINSWITH[c]", word]]; } BOOL show = (self.searchResult.count > 0); [self showAutoCompletionView:show]; } ``` ##### Swift ```swift override func didChangeAutoCompletionPrefix(prefix: String, andWord word: String) { let array: NSArray = self.channels if prefix == "#" && word.characters.count > 0 { self.searchResult = array.filteredArrayUsingPredicate(NSPredicate(format: "self BEGINSWITH[c] %@", word)) } let show = (self.searchResult.count > 0) self.showAutoCompletionView(show) } ``` The autocompletion view is a `UITableView` instance, so you will need to use `UITableViewDataSource` to populate its cells. You have complete freedom for customizing the cells. You don't need to call `reloadData` yourself, since it will be invoked automatically right after calling the `showAutoCompletionView` method. #### 3. Layout The maximum height of the autocompletion view is set to 140 pts by default. You can update this value anytime, so the view automatically adjusts based on the amount of displayed cells. ##### Obj-C ```objc - (CGFloat)heightForAutoCompletionView { CGFloat cellHeight = 34.0; return cellHeight*self.searchResult.count; } ``` ##### Swift ```swift override func heightForAutoCompletionView() -> CGFloat { let cellHeight:CGFloat = 34 return cellHeight * CGFloat(self.searchResult.count) } ``` #### 4. Confirmation If the user selects any autocompletion view cell on `tableView:didSelectRowAtIndexPath:`, you must call `acceptAutoCompletionWithString:` to commit autocompletion. That method expects a string matching the selected item, that you would like to be inserted in the text view. ##### Obj-C ```objc - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if ([tableView isEqual:self.autoCompletionView]) { NSMutableString *item = [self.searchResult[indexPath.row] mutableCopy]; [item appendString:@" "]; // Adding a space helps dismissing the auto-completion view [self acceptAutoCompletionWithString:item keepPrefix:YES]; } } ``` ##### Swift ```swift override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { if tableView.isEqual(self.autoCompletionView) { var item = self.searchResult[indexPath.row] item += " " // Adding a space helps dismissing the auto-completion view self.acceptAutoCompletionWithString(item) } } ``` The autocompletion view will automatically be dismissed and the chosen string will be inserted in the text view, replacing the detected prefix and word. You can always call `cancelAutoCompletion` to exit the autocompletion mode and refresh the UI. ### Edit Mode ![Edit Mode](Screenshots/screenshot_edit-mode.png) To enable edit mode, you simply need to call `editText:`, and the text input will switch to edit mode, removing both left and right buttons, extending the input bar a bit higher with "Accept" and "Cancel" buttons. Both of this buttons are accessible in the `SLKTextInputbar` instance for customisation. To capture the "Accept" or "Cancel" events, you must override the following methods. ##### Obj-C ```objc - (void)didCommitTextEditing:(id)sender { NSString *message = [self.textView.text copy]; [self.messages removeObjectAtIndex:0]; [self.messages insertObject:message atIndex:0]; [self.tableView reloadData]; [super didCommitTextEditing:sender]; } - (void)didCancelTextEditing:(id)sender { [super didCancelTextEditing:sender]; } ``` ##### Swift ```swift override func didCommitTextEditing(sender: AnyObject) { let message:String = self.textView.text self.messages.removeAtIndex(0) self.messages.insert(message, atIndex: 0) self.tableView!.reloadData() super.didCommitTextEditing(sender) } override func didCancelTextEditing(sender: AnyObject) { super.didCancelTextEditing(sender) } ``` Notice that you must call `super` at some point, so the text input exits the edit mode, re-adjusting the layout and clearing the text view. Use the `editing` property to know if the editing mode is on. ### Markdown Formatting ![Markdown Formatting](Screenshots/screenshot_markdown-formatting.png) You can register markdown formatting symbols so they can easily be used to wrap a text selection, with the help of the native contextual menu, aka `UIMenuController`. This feature doesn't take care of the rendering of the markdown: it's sole purpose is to ease the formatting tools to the user. Optionally, you can enable `autoCompleteFormatting` so any pending markdown closure symbol can be added automatically after double tapping on the keyboard spacebar, just like the native gesture to add a sentence period. The sentence period is still being added as a fallback. ![Markdown Formatting Animated](Screenshots/screenshot_markdown-formatting.gif) #### 1. Registration You must first register the formatting symbol and assign a title string to be used in the menu controller item. ##### Obj-C ```objc [self.textView registerMarkdownFormattingSymbol:@"*" withTitle:@"Bold"]; ``` ##### Swift ```swift self.textView.registerMarkdownFormattingSymbol("*", withTitle: "Bold") ``` #### 2. Customisation Futher more, you can customise some of the behavior for special formatting cases, using the `UITextViewDelegate` methods. In the following example, we don't present the Quote formatting in the contextual menu when the text selection isn't a paragraph. ##### Obj-C ```objc - (BOOL)textView:(SLKTextView *)textView shouldOfferFormattingForSymbol:(NSString *)symbol { if ([symbol isEqualToString:@">"]) { NSRange selection = textView.selectedRange; // The Quote formatting only applies new paragraphs if (selection.location == 0 && selection.length > 0) { return YES; } // or older paragraphs too NSString *prevString = [textView.text substringWithRange:NSMakeRange(selection.location-1, 1)]; if ([[NSCharacterSet newlineCharacterSet] characterIsMember:[prevString characterAtIndex:0]]) { return YES; } return NO; } return [super textView:textView shouldOfferFormattingForSymbol:symbol]; } ``` In this other method implementation, we don't want to allow auto-completion for the Quote formatting since it doesn't require a closure. ##### Obj-C ```objc - (BOOL)textView:(SLKTextView *)textView shouldInsertSuffixForFormattingWithSymbol:(NSString *)symbol prefixRange:(NSRange)prefixRange { if ([symbol isEqualToString:@">"]) { return NO; } return [super textView:textView shouldInsertSuffixForFormattingWithSymbol:symbol prefixRange:prefixRange]; } ``` ### Typing Indicator ![Typing Indicator](Screenshots/screenshot_typing-indicator.png) Optionally, you can enable a simple typing indicator, which will be displayed right above the text input. It shows the name of the people that are typing, and if more than 2, it will display "Several are typing" message. To enable the typing indicator, just call: ##### Obj-C ```objc [self.typingIndicatorView insertUsername:@"John"]; ``` ##### Swift ```swift self.typingIndicatorView?.insertUsername("John") ``` and the view will automatically be animated on top of the text input. After a default interval of 6 seconds, if the same name hasn't been assigned once more, the view will be dismissed with animation. You can remove names from the list by calling: ##### Obj-C ```objc [self.typingIndicatorView removeUsername:@"John"]; ``` ##### Swift ```swift self.typingIndicatorView?.removeUsername("John") ``` You can also dismiss it by calling: ##### Obj-C ```objc [self.typingIndicatorView dismissIndicator]; ``` ##### Swift ```swift self.typingIndicatorView?.dismissIndicator() ``` ### Panning Gesture Dismissing the keyboard with a panning gesture is enabled by default with the `keyboardPanningEnabled` property. You can always disable it if you'd like. You can extend the `verticalPanGesture` behaviors with the `UIGestureRecognizerDelegate` methods. ### Hideable TextInputbar Sometimes you may need to hide the text input bar. Very similar to `UINavigationViewController`'s API, simply do: ##### Obj-C ```objc [self setTextInputbarHidden:YES animated:YES]; ``` ##### Swift ```swift self.setTextInputbarHidden(true, animated: true) ``` ### Shake Gesture ![Shake Gesture](Screenshots/screenshot_shake-undo.png) A shake gesture to clear text is enabled by default with the `undoShakingEnabled` property. You can optionally override `willRequestUndo`, to implement your UI to ask the users if he would like to clean the text view's text. If there is not text entered, the method will not be called. If you don't override `willRequestUndo` and `undoShakingEnabled` is set to `YES`/`true`, a system alert will be shown. ### External Keyboard There a few basic key commands enabled by default: - cmd + z -> undo - shift + cmd + z -> redo - return key -> calls `didPressRightButton:`, or `didCommitTextEditing:` if in edit mode - shift/cmd + return key -> line break - escape key -> exits edit mode, or auto-completion mode, or dismisses the keyboard - up & down arrows -> vertical cursor movement To add additional key commands, simply override `keyCommands` and append `super`'s array. ##### Obj-C ```objc - (NSArray *)keyCommands { NSMutableArray *commands = [NSMutableArray arrayWithArray:[super keyCommands]]; // Edit last message [commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:0 action:@selector(editLastMessage:)]]; return commands; } ``` ##### Swift ```swift override var keyCommands: [UIKeyCommand]? { var commands = super.keyCommands // Edit last message let command = UIKeyCommand(input: UIKeyInputUpArrow, modifierFlags: .Command, action: "editLastMessage:") commands?.append(command) return commands } ``` There are also a set of useful flags for keyboard special detections such as `isExternalKeyboardDetected`, `isKeyboardUndocked`, `typingSuggestionEnabled` and `isTrackpadEnabled` (iOS 9 only) ### Dynamic Type Dynamic Type is enabled by default with the `dynamicTypeEnabled` property. You can always disable it if you'd like, but the text input bar would still adjust to best fit the font size of the text view. ![Dynamic-Type](Screenshots/screenshot_dynamic-type.png) ### Xcode Templates ![Template](Screenshots/screenshot_template.png) We have prepared a set of useful Xcode templates so you can quickly start using SlackTextViewController. To install them, open up your terminal and type: ```bash sh ./SlackTextViewController/File\ Templates/install.sh ``` These templates are also available in [Alcatraz](https://github.com/alcatraz/Alcatraz). ================================================ FILE: SlackTextViewController/SlackTextViewController/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: SlackTextViewController/SlackTextViewController/SlackTextViewController.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import FOUNDATION_EXPORT double SlackTextViewControllerVersionNumber; FOUNDATION_EXPORT const unsigned char SlackTextViewControllerVersionString[]; #import #import #import #import #import #import #import #import #import #import #import ================================================ FILE: SlackTextViewController/SlackTextViewController.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ F52253811CBE2F85002EECA9 /* SLKInputAccessoryView.h in Headers */ = {isa = PBXBuildFile; fileRef = F522536B1CBE2F85002EECA9 /* SLKInputAccessoryView.h */; settings = {ATTRIBUTES = (Public, ); }; }; F52253821CBE2F85002EECA9 /* SLKInputAccessoryView.m in Sources */ = {isa = PBXBuildFile; fileRef = F522536C1CBE2F85002EECA9 /* SLKInputAccessoryView.m */; }; F52253831CBE2F85002EECA9 /* SLKTextInput.h in Headers */ = {isa = PBXBuildFile; fileRef = F522536D1CBE2F85002EECA9 /* SLKTextInput.h */; settings = {ATTRIBUTES = (Public, ); }; }; F52253841CBE2F85002EECA9 /* SLKTextInput+Implementation.m in Sources */ = {isa = PBXBuildFile; fileRef = F522536E1CBE2F85002EECA9 /* SLKTextInput+Implementation.m */; }; F52253851CBE2F85002EECA9 /* SLKTextInputbar.h in Headers */ = {isa = PBXBuildFile; fileRef = F522536F1CBE2F85002EECA9 /* SLKTextInputbar.h */; settings = {ATTRIBUTES = (Public, ); }; }; F52253861CBE2F85002EECA9 /* SLKTextInputbar.m in Sources */ = {isa = PBXBuildFile; fileRef = F52253701CBE2F85002EECA9 /* SLKTextInputbar.m */; }; F52253871CBE2F85002EECA9 /* SLKTextView.h in Headers */ = {isa = PBXBuildFile; fileRef = F52253711CBE2F85002EECA9 /* SLKTextView.h */; settings = {ATTRIBUTES = (Public, ); }; }; F52253881CBE2F85002EECA9 /* SLKTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = F52253721CBE2F85002EECA9 /* SLKTextView.m */; }; F52253891CBE2F85002EECA9 /* SLKTextView+SLKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = F52253731CBE2F85002EECA9 /* SLKTextView+SLKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; F522538A1CBE2F85002EECA9 /* SLKTextView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = F52253741CBE2F85002EECA9 /* SLKTextView+SLKAdditions.m */; }; F522538B1CBE2F85002EECA9 /* SLKTextViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = F52253751CBE2F85002EECA9 /* SLKTextViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; F522538C1CBE2F85002EECA9 /* SLKTextViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F52253761CBE2F85002EECA9 /* SLKTextViewController.m */; }; F522538D1CBE2F85002EECA9 /* SLKTypingIndicatorProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = F52253771CBE2F85002EECA9 /* SLKTypingIndicatorProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; F522538E1CBE2F85002EECA9 /* SLKTypingIndicatorView.h in Headers */ = {isa = PBXBuildFile; fileRef = F52253781CBE2F85002EECA9 /* SLKTypingIndicatorView.h */; settings = {ATTRIBUTES = (Public, ); }; }; F522538F1CBE2F85002EECA9 /* SLKTypingIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = F52253791CBE2F85002EECA9 /* SLKTypingIndicatorView.m */; }; F52253901CBE2F85002EECA9 /* SLKUIConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = F522537A1CBE2F85002EECA9 /* SLKUIConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; F52253911CBE2F85002EECA9 /* UIResponder+SLKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = F522537B1CBE2F85002EECA9 /* UIResponder+SLKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; F52253921CBE2F85002EECA9 /* UIResponder+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = F522537C1CBE2F85002EECA9 /* UIResponder+SLKAdditions.m */; }; F52253931CBE2F85002EECA9 /* UIScrollView+SLKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = F522537D1CBE2F85002EECA9 /* UIScrollView+SLKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; F52253941CBE2F85002EECA9 /* UIScrollView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = F522537E1CBE2F85002EECA9 /* UIScrollView+SLKAdditions.m */; }; F52253951CBE2F85002EECA9 /* UIView+SLKAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = F522537F1CBE2F85002EECA9 /* UIView+SLKAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; F52253961CBE2F85002EECA9 /* UIView+SLKAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = F52253801CBE2F85002EECA9 /* UIView+SLKAdditions.m */; }; F5A782781BD0CEF300EC230B /* SlackTextViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = F5A782771BD0CEF300EC230B /* SlackTextViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; F5B4E7931C4DA053005CBBE0 /* SlackTextViewController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5A782741BD0CEF300EC230B /* SlackTextViewController.framework */; }; F5B4E79C1C4DA098005CBBE0 /* FrameworkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F5B4E79B1C4DA098005CBBE0 /* FrameworkTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ F5B4E7941C4DA053005CBBE0 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F5A7826B1BD0CEF300EC230B /* Project object */; proxyType = 1; remoteGlobalIDString = F5A782731BD0CEF300EC230B; remoteInfo = SlackTextViewController; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ F522536B1CBE2F85002EECA9 /* SLKInputAccessoryView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKInputAccessoryView.h; sourceTree = ""; }; F522536C1CBE2F85002EECA9 /* SLKInputAccessoryView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLKInputAccessoryView.m; sourceTree = ""; }; F522536D1CBE2F85002EECA9 /* SLKTextInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTextInput.h; sourceTree = ""; }; F522536E1CBE2F85002EECA9 /* SLKTextInput+Implementation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SLKTextInput+Implementation.m"; sourceTree = ""; }; F522536F1CBE2F85002EECA9 /* SLKTextInputbar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTextInputbar.h; sourceTree = ""; }; F52253701CBE2F85002EECA9 /* SLKTextInputbar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLKTextInputbar.m; sourceTree = ""; }; F52253711CBE2F85002EECA9 /* SLKTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTextView.h; sourceTree = ""; }; F52253721CBE2F85002EECA9 /* SLKTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLKTextView.m; sourceTree = ""; }; F52253731CBE2F85002EECA9 /* SLKTextView+SLKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SLKTextView+SLKAdditions.h"; sourceTree = ""; }; F52253741CBE2F85002EECA9 /* SLKTextView+SLKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SLKTextView+SLKAdditions.m"; sourceTree = ""; }; F52253751CBE2F85002EECA9 /* SLKTextViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTextViewController.h; sourceTree = ""; }; F52253761CBE2F85002EECA9 /* SLKTextViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLKTextViewController.m; sourceTree = ""; }; F52253771CBE2F85002EECA9 /* SLKTypingIndicatorProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTypingIndicatorProtocol.h; sourceTree = ""; }; F52253781CBE2F85002EECA9 /* SLKTypingIndicatorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKTypingIndicatorView.h; sourceTree = ""; }; F52253791CBE2F85002EECA9 /* SLKTypingIndicatorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLKTypingIndicatorView.m; sourceTree = ""; }; F522537A1CBE2F85002EECA9 /* SLKUIConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLKUIConstants.h; sourceTree = ""; }; F522537B1CBE2F85002EECA9 /* UIResponder+SLKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIResponder+SLKAdditions.h"; sourceTree = ""; }; F522537C1CBE2F85002EECA9 /* UIResponder+SLKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIResponder+SLKAdditions.m"; sourceTree = ""; }; F522537D1CBE2F85002EECA9 /* UIScrollView+SLKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+SLKAdditions.h"; sourceTree = ""; }; F522537E1CBE2F85002EECA9 /* UIScrollView+SLKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+SLKAdditions.m"; sourceTree = ""; }; F522537F1CBE2F85002EECA9 /* UIView+SLKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+SLKAdditions.h"; sourceTree = ""; }; F52253801CBE2F85002EECA9 /* UIView+SLKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+SLKAdditions.m"; sourceTree = ""; }; F5A782741BD0CEF300EC230B /* SlackTextViewController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SlackTextViewController.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F5A782771BD0CEF300EC230B /* SlackTextViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SlackTextViewController.h; sourceTree = ""; }; F5A782791BD0CEF400EC230B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F5B4E78E1C4DA053005CBBE0 /* SlackTextViewControllerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SlackTextViewControllerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F5B4E7921C4DA053005CBBE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F5B4E79B1C4DA098005CBBE0 /* FrameworkTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FrameworkTests.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F5A782701BD0CEF300EC230B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; F5B4E78B1C4DA053005CBBE0 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F5B4E7931C4DA053005CBBE0 /* SlackTextViewController.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ F5A7826A1BD0CEF300EC230B = { isa = PBXGroup; children = ( F5A782761BD0CEF300EC230B /* SlackTextViewController */, F5B4E78F1C4DA053005CBBE0 /* SlackTextViewControllerTests */, F5A782751BD0CEF300EC230B /* Products */, ); sourceTree = ""; }; F5A782751BD0CEF300EC230B /* Products */ = { isa = PBXGroup; children = ( F5A782741BD0CEF300EC230B /* SlackTextViewController.framework */, F5B4E78E1C4DA053005CBBE0 /* SlackTextViewControllerTests.xctest */, ); name = Products; sourceTree = ""; }; F5A782761BD0CEF300EC230B /* SlackTextViewController */ = { isa = PBXGroup; children = ( F5A782771BD0CEF300EC230B /* SlackTextViewController.h */, F5A7828F1BD0CF1D00EC230B /* Source */, F5A782791BD0CEF400EC230B /* Info.plist */, ); path = SlackTextViewController; sourceTree = ""; }; F5A7828F1BD0CF1D00EC230B /* Source */ = { isa = PBXGroup; children = ( F522536B1CBE2F85002EECA9 /* SLKInputAccessoryView.h */, F522536C1CBE2F85002EECA9 /* SLKInputAccessoryView.m */, F522536D1CBE2F85002EECA9 /* SLKTextInput.h */, F522536E1CBE2F85002EECA9 /* SLKTextInput+Implementation.m */, F522536F1CBE2F85002EECA9 /* SLKTextInputbar.h */, F52253701CBE2F85002EECA9 /* SLKTextInputbar.m */, F52253711CBE2F85002EECA9 /* SLKTextView.h */, F52253721CBE2F85002EECA9 /* SLKTextView.m */, F52253731CBE2F85002EECA9 /* SLKTextView+SLKAdditions.h */, F52253741CBE2F85002EECA9 /* SLKTextView+SLKAdditions.m */, F52253751CBE2F85002EECA9 /* SLKTextViewController.h */, F52253761CBE2F85002EECA9 /* SLKTextViewController.m */, F52253771CBE2F85002EECA9 /* SLKTypingIndicatorProtocol.h */, F52253781CBE2F85002EECA9 /* SLKTypingIndicatorView.h */, F52253791CBE2F85002EECA9 /* SLKTypingIndicatorView.m */, F522537A1CBE2F85002EECA9 /* SLKUIConstants.h */, F522537B1CBE2F85002EECA9 /* UIResponder+SLKAdditions.h */, F522537C1CBE2F85002EECA9 /* UIResponder+SLKAdditions.m */, F522537D1CBE2F85002EECA9 /* UIScrollView+SLKAdditions.h */, F522537E1CBE2F85002EECA9 /* UIScrollView+SLKAdditions.m */, F522537F1CBE2F85002EECA9 /* UIView+SLKAdditions.h */, F52253801CBE2F85002EECA9 /* UIView+SLKAdditions.m */, ); name = Source; path = ../../Source; sourceTree = ""; }; F5B4E78F1C4DA053005CBBE0 /* SlackTextViewControllerTests */ = { isa = PBXGroup; children = ( F5B4E79B1C4DA098005CBBE0 /* FrameworkTests.m */, F5B4E7921C4DA053005CBBE0 /* Info.plist */, ); path = SlackTextViewControllerTests; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ F5A782711BD0CEF300EC230B /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( F52253811CBE2F85002EECA9 /* SLKInputAccessoryView.h in Headers */, F5A782781BD0CEF300EC230B /* SlackTextViewController.h in Headers */, F52253831CBE2F85002EECA9 /* SLKTextInput.h in Headers */, F52253871CBE2F85002EECA9 /* SLKTextView.h in Headers */, F522538E1CBE2F85002EECA9 /* SLKTypingIndicatorView.h in Headers */, F52253901CBE2F85002EECA9 /* SLKUIConstants.h in Headers */, F52253911CBE2F85002EECA9 /* UIResponder+SLKAdditions.h in Headers */, F52253891CBE2F85002EECA9 /* SLKTextView+SLKAdditions.h in Headers */, F522538B1CBE2F85002EECA9 /* SLKTextViewController.h in Headers */, F52253931CBE2F85002EECA9 /* UIScrollView+SLKAdditions.h in Headers */, F522538D1CBE2F85002EECA9 /* SLKTypingIndicatorProtocol.h in Headers */, F52253951CBE2F85002EECA9 /* UIView+SLKAdditions.h in Headers */, F52253851CBE2F85002EECA9 /* SLKTextInputbar.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ F5A782731BD0CEF300EC230B /* SlackTextViewController */ = { isa = PBXNativeTarget; buildConfigurationList = F5A782881BD0CEF400EC230B /* Build configuration list for PBXNativeTarget "SlackTextViewController" */; buildPhases = ( F5A7826F1BD0CEF300EC230B /* Sources */, F5A782701BD0CEF300EC230B /* Frameworks */, F5A782711BD0CEF300EC230B /* Headers */, F5A782721BD0CEF300EC230B /* Resources */, ); buildRules = ( ); dependencies = ( ); name = SlackTextViewController; productName = SlackTextViewController; productReference = F5A782741BD0CEF300EC230B /* SlackTextViewController.framework */; productType = "com.apple.product-type.framework"; }; F5B4E78D1C4DA053005CBBE0 /* SlackTextViewControllerTests */ = { isa = PBXNativeTarget; buildConfigurationList = F5B4E7961C4DA053005CBBE0 /* Build configuration list for PBXNativeTarget "SlackTextViewControllerTests" */; buildPhases = ( F5B4E78A1C4DA053005CBBE0 /* Sources */, F5B4E78B1C4DA053005CBBE0 /* Frameworks */, F5B4E78C1C4DA053005CBBE0 /* Resources */, ); buildRules = ( ); dependencies = ( F5B4E7951C4DA053005CBBE0 /* PBXTargetDependency */, ); name = SlackTextViewControllerTests; productName = UnitTests; productReference = F5B4E78E1C4DA053005CBBE0 /* SlackTextViewControllerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F5A7826B1BD0CEF300EC230B /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 0900; ORGANIZATIONNAME = "Slack Technologies, Inc."; TargetAttributes = { F5A782731BD0CEF300EC230B = { CreatedOnToolsVersion = 7.0.1; }; F5B4E78D1C4DA053005CBBE0 = { CreatedOnToolsVersion = 7.2; }; }; }; buildConfigurationList = F5A7826E1BD0CEF300EC230B /* Build configuration list for PBXProject "SlackTextViewController" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F5A7826A1BD0CEF300EC230B; productRefGroup = F5A782751BD0CEF300EC230B /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F5A782731BD0CEF300EC230B /* SlackTextViewController */, F5B4E78D1C4DA053005CBBE0 /* SlackTextViewControllerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F5A782721BD0CEF300EC230B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; F5B4E78C1C4DA053005CBBE0 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F5A7826F1BD0CEF300EC230B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F52253941CBE2F85002EECA9 /* UIScrollView+SLKAdditions.m in Sources */, F522538F1CBE2F85002EECA9 /* SLKTypingIndicatorView.m in Sources */, F52253921CBE2F85002EECA9 /* UIResponder+SLKAdditions.m in Sources */, F52253841CBE2F85002EECA9 /* SLKTextInput+Implementation.m in Sources */, F522538A1CBE2F85002EECA9 /* SLKTextView+SLKAdditions.m in Sources */, F52253861CBE2F85002EECA9 /* SLKTextInputbar.m in Sources */, F52253881CBE2F85002EECA9 /* SLKTextView.m in Sources */, F52253961CBE2F85002EECA9 /* UIView+SLKAdditions.m in Sources */, F522538C1CBE2F85002EECA9 /* SLKTextViewController.m in Sources */, F52253821CBE2F85002EECA9 /* SLKInputAccessoryView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; F5B4E78A1C4DA053005CBBE0 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F5B4E79C1C4DA098005CBBE0 /* FrameworkTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ F5B4E7951C4DA053005CBBE0 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F5A782731BD0CEF300EC230B /* SlackTextViewController */; targetProxy = F5B4E7941C4DA053005CBBE0 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ F5A782861BD0CEF400EC230B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; F5A782871BD0CEF400EC230B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; F5A782891BD0CEF400EC230B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SlackTextViewController/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.slack.SlackTextViewController; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; }; name = Debug; }; F5A7828A1BD0CEF400EC230B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SlackTextViewController/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.slack.SlackTextViewController; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; }; name = Release; }; F5B4E7971C4DA053005CBBE0 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = SlackTextViewControllerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.slack.SlackTextViewControllerTests; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; F5B4E7981C4DA053005CBBE0 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = SlackTextViewControllerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.slack.SlackTextViewControllerTests; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F5A7826E1BD0CEF300EC230B /* Build configuration list for PBXProject "SlackTextViewController" */ = { isa = XCConfigurationList; buildConfigurations = ( F5A782861BD0CEF400EC230B /* Debug */, F5A782871BD0CEF400EC230B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F5A782881BD0CEF400EC230B /* Build configuration list for PBXNativeTarget "SlackTextViewController" */ = { isa = XCConfigurationList; buildConfigurations = ( F5A782891BD0CEF400EC230B /* Debug */, F5A7828A1BD0CEF400EC230B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F5B4E7961C4DA053005CBBE0 /* Build configuration list for PBXNativeTarget "SlackTextViewControllerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( F5B4E7971C4DA053005CBBE0 /* Debug */, F5B4E7981C4DA053005CBBE0 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = F5A7826B1BD0CEF300EC230B /* Project object */; } ================================================ FILE: SlackTextViewController/SlackTextViewController.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: SlackTextViewController/SlackTextViewController.xcodeproj/xcshareddata/xcbaselines/F5B4E78D1C4DA053005CBBE0.xcbaseline/F8445E9E-5B76-4616-90F9-7D94D0E1142F.plist ================================================ classNames SlackTextViewControllerTests testCollectionViewVersion com.apple.XCTPerformanceMetric_WallClockTime baselineAverage 0.0041676 baselineIntegrationDisplayName Local Baseline ================================================ FILE: SlackTextViewController/SlackTextViewController.xcodeproj/xcshareddata/xcbaselines/F5B4E78D1C4DA053005CBBE0.xcbaseline/Info.plist ================================================ runDestinationsByUUID F8445E9E-5B76-4616-90F9-7D94D0E1142F localComputer busSpeedInMHz 100 cpuCount 1 cpuKind Intel Core i7 cpuSpeedInMHz 2900 logicalCPUCoresPerPackage 8 modelCode MacBookPro13,3 physicalCPUCoresPerPackage 4 platformIdentifier com.apple.platform.macosx targetArchitecture x86_64 targetDevice modelCode iPhone10,5 platformIdentifier com.apple.platform.iphonesimulator ================================================ FILE: SlackTextViewController/SlackTextViewController.xcodeproj/xcshareddata/xcschemes/SlackTextViewController.xcscheme ================================================ ================================================ FILE: SlackTextViewController/SlackTextViewController.xcodeproj/xcshareddata/xcschemes/SlackTextViewControllerTests.xcscheme ================================================ ================================================ FILE: SlackTextViewController/SlackTextViewControllerTests/FrameworkTests.m ================================================ // // SlackTextViewControllerTests.m // SlackTextViewControllerTests // // Created by Ignacio Romero Z. on 3/20/15. // Copyright (c) 2015 Slack. All rights reserved. // #import #import @interface SLKTextViewControllerTest : SLKTextViewController @end @implementation SLKTextViewControllerTest @end @interface SlackTextViewControllerTests : XCTestCase @end @implementation SlackTextViewControllerTests - (void)setUp { [super setUp]; } - (void)tearDown { [super tearDown]; } - (void)testSubclassing { XCTAssertThrows([SLKTextViewController new]); } - (void)testTableViewVersion { SLKTextViewControllerTest *controller = [[SLKTextViewControllerTest alloc] initWithTableViewStyle:UITableViewStylePlain]; XCTAssertNotNil(controller.tableView, @"Cannot create SLKTextViewController instance!"); } - (void)testCollectionViewVersion { UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; SLKTextViewControllerTest *controller = [[SLKTextViewControllerTest alloc] initWithCollectionViewLayout:layout]; XCTAssertNotNil(controller.collectionView, @"Cannot create SLKTextViewController instance!"); } - (void)testScrollViewVersion { UIScrollView *scrollView = [UIScrollView new]; SLKTextViewControllerTest *controller = [[SLKTextViewControllerTest alloc] initWithScrollView:scrollView]; XCTAssertNotNil(controller.scrollView, @"Cannot create SLKTextViewController instance!"); } @end ================================================ FILE: SlackTextViewController/SlackTextViewControllerTests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: SlackTextViewController.podspec ================================================ @version = "1.9.6" Pod::Spec.new do |s| s.name = "SlackTextViewController" s.version = @version s.summary = "A drop-in UIViewController subclass with a custom growing text input and other useful messaging features." s.description = "Meant to be a replacement for UITableViewController & UICollectionViewController. This library is used in Slack's iOS app. It was built to fit our needs, but is flexible enough to be reused by others wanting to build great messaging apps for iOS." s.homepage = "https://slack.com/" s.screenshots = "https://github.com/slackhq/SlackTextViewController/raw/master/Screenshots/slacktextviewcontroller_demo.gif" s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { "Slack Technologies, Inc." => "ios-team@slack-corp.com" } s.source = { :git => "https://github.com/slackhq/SlackTextViewController.git", :tag => "v#{s.version}" } s.frameworks = 'CoreGraphics', 'UIKit' s.platform = :ios, "7.0" s.requires_arc = true s.header_mappings_dir = 'Source' s.source_files = 'Source/**/*.{h,m}' end ================================================ FILE: Source/SLKInputAccessoryView.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import @interface SLKInputAccessoryView : UIView /* The system keyboard view used as reference. */ @property (nonatomic, weak, readonly) UIView *_Nullable keyboardViewProxy; @end ================================================ FILE: Source/SLKInputAccessoryView.m ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import "SLKInputAccessoryView.h" #import "SLKUIConstants.h" @implementation SLKInputAccessoryView #pragma mark - Super Overrides - (void)willMoveToSuperview:(UIView *)newSuperview { if (!SLK_IS_IOS9_AND_HIGHER) { _keyboardViewProxy = newSuperview; } } @end ================================================ FILE: Source/SLKTextInput+Implementation.m ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import "SLKTextInput.h" /** Implementing SLKTextInput methods in a generic NSObject helps reusing the same logic for any SLKTextInput conformant class. This is the closest and cleanest technique to extend protocol's default implementations, like you'd do in Swift. */ @interface NSObject (SLKTextInput) @end @implementation NSObject (SLKTextInput) #pragma mark - Public Methods - (void)lookForPrefixes:(NSSet *)prefixes completion:(void (^)(NSString *prefix, NSString *word, NSRange wordRange))completion { if (![self conformsToProtocol:@protocol(SLKTextInput)]) { return; } NSAssert([prefixes isKindOfClass:[NSSet class]], @"You must provide a set containing String prefixes."); NSAssert(completion != nil, @"You must provide a non-nil completion block."); // Skip when there is no prefixes to look for. if (prefixes.count == 0) { return; } NSRange wordRange; NSString *word = [self wordAtCaretRange:&wordRange]; if (word.length > 0) { for (NSString *prefix in prefixes) { if ([word hasPrefix:prefix]) { if (completion) { completion(prefix, word, wordRange); } return; } } } // Fallback to an empty callback if (completion) { completion(nil, nil, NSMakeRange(0,0)); } } - (NSString *)wordAtCaretRange:(NSRangePointer)range { return [self wordAtRange:[self slk_caretRange] rangeInText:range]; } - (NSString *)wordAtRange:(NSRange)range rangeInText:(NSRangePointer)rangePointer { if (![self conformsToProtocol:@protocol(SLKTextInput)]) { return nil; } NSInteger location = range.location; if (location == NSNotFound) { return nil; } NSString *text = [self slk_text]; // Aborts in case minimum requieres are not fufilled if (text.length == 0 || location < 0 || (range.location+range.length) > text.length) { *rangePointer = NSMakeRange(0, 0); return nil; } NSString *leftPortion = [text substringToIndex:location]; NSArray *leftComponents = [leftPortion componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSString *leftWordPart = [leftComponents lastObject]; NSString *rightPortion = [text substringFromIndex:location]; NSArray *rightComponents = [rightPortion componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSString *rightPart = [rightComponents firstObject]; if (location > 0) { NSString *characterBeforeCursor = [text substringWithRange:NSMakeRange(location-1, 1)]; NSRange whitespaceRange = [characterBeforeCursor rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]]; if (whitespaceRange.length == 1) { // At the start of a word, just use the word behind the cursor for the current word *rangePointer = NSMakeRange(location, rightPart.length); return rightPart; } } // In the middle of a word, so combine the part of the word before the cursor, and after the cursor to get the current word *rangePointer = NSMakeRange(location-leftWordPart.length, leftWordPart.length+rightPart.length); NSString *word = [leftWordPart stringByAppendingString:rightPart]; NSString *linebreak = @"\n"; // If a break is detected, return the last component of the string if ([word rangeOfString:linebreak].location != NSNotFound) { *rangePointer = [text rangeOfString:word]; word = [[word componentsSeparatedByString:linebreak] lastObject]; } return word; } #pragma mark - Private Methods - (NSString *)slk_text { if (![self conformsToProtocol:@protocol(SLKTextInput)]) { return nil; } idinput = (id)self; UITextRange *textRange = [input textRangeFromPosition:input.beginningOfDocument toPosition:input.endOfDocument]; return [input textInRange:textRange]; } - (NSRange)slk_caretRange { if (![self conformsToProtocol:@protocol(SLKTextInput)]) { return NSMakeRange(0,0); } idinput = (id)self; UITextPosition *beginning = input.beginningOfDocument; UITextRange *selectedRange = input.selectedTextRange; UITextPosition *selectionStart = selectedRange.start; UITextPosition *selectionEnd = selectedRange.end; const NSInteger location = [input offsetFromPosition:beginning toPosition:selectionStart]; const NSInteger length = [input offsetFromPosition:selectionStart toPosition:selectionEnd]; return NSMakeRange(location, length); } @end ================================================ FILE: Source/SLKTextInput.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import /** Classes that adopt the SLKTextInput protocol interact with the text input system and thus acquire features such as text processing. All these methods are already implemented in SLKTextInput+Implementation.m */ @protocol SLKTextInput @optional /** Searches for any matching string prefix at the text input's caret position. When nothing found, the completion block returns nil values. This implementation is internally performed on a background thread and forwarded to the main thread once completed. @param prefixes A set of prefixes to search for. @param completion A completion block called whenever the text processing finishes, successfuly or not. Required. */ - (void)lookForPrefixes:(NSSet *)prefixes completion:(void (^)(NSString *prefix, NSString *word, NSRange wordRange))completion; /** Finds the word close to the caret's position, if any. @param range Returns the range of the found word. @returns The found word. */ - (NSString *)wordAtCaretRange:(NSRangePointer)range; /** Finds the word close to specific range. @param range The range to be used for searching the word. @param rangePointer Returns the range of the found word. @returns The found word. */ - (NSString *)wordAtRange:(NSRange)range rangeInText:(NSRangePointer)rangePointer; @end ================================================ FILE: Source/SLKTextInputbar.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import @class SLKTextView; @class SLKInputAccessoryView; typedef NS_ENUM(NSUInteger, SLKCounterStyle) { SLKCounterStyleNone, SLKCounterStyleSplit, SLKCounterStyleCountdown, SLKCounterStyleCountdownReversed }; typedef NS_ENUM(NSUInteger, SLKCounterPosition) { SLKCounterPositionTop, SLKCounterPositionBottom }; NS_ASSUME_NONNULL_BEGIN /** @name A custom tool bar encapsulating messaging controls. */ @interface SLKTextInputbar : UIToolbar /** The centered text input view. The maximum number of lines is configured by default, to best fit each devices dimensions. For iPhone 4 (<=480pts): 4 lines For iPhone 5 & 6 (>=568pts): 6 lines For iPad (>=768pts): 8 lines */ @property (nonatomic, readonly, strong) SLKTextView *textView; /** Optional view to host outlets under the text view, adjusting its height based on its subviews. Non-visible by default. Subviews' layout should be configured using auto-layout as well. */ @property (nonatomic, readonly, strong) UIView *contentView; /** The custom input accessory view, used as empty achor view to detect the keyboard frame. */ @property (nonatomic, readonly, strong) SLKInputAccessoryView *inputAccessoryView; /** The left action button action. */ @property (nonatomic, strong) UIButton *leftButton; /** The right action button action. */ @property (nonatomic, strong) UIButton *rightButton; /** YES if the right button should be hidden animatedly in case the text view has no text in it. Default is YES. */ @property (nonatomic, readwrite) BOOL autoHideRightButton; /** YES if animations should have bouncy effects. Default is YES. */ @property (nonatomic, assign) BOOL bounces; /** The inner padding to use when laying out content in the view. Default is {5, 8, 5, 8}. */ @property (nonatomic, assign) UIEdgeInsets contentInset; /** The minimum height based on the intrinsic content size's. */ @property (nonatomic, readonly) CGFloat minimumInputbarHeight; /** The most appropriate height calculated based on the amount of lines of text and other factors. */ @property (nonatomic, readonly) CGFloat appropriateHeight; #pragma mark - Initialization ///------------------------------------------------ /// @name Initialization ///------------------------------------------------ /** Initializes a text input bar with a class to be used for the text view @param textViewClass The class to be used when creating the text view. May be nil. If provided, the class must be a subclass of SLKTextView @return An initialized SLKTextInputbar object or nil if the object could not be created. */ - (instancetype)initWithTextViewClass:(Class)textViewClass; #pragma mark - Text Editing ///------------------------------------------------ /// @name Text Editing ///------------------------------------------------ /** The view displayed on top if the text input bar, containing the button outlets, when editing is enabled. */ @property (nonatomic, strong) UIView *editorContentView; /** The title label displayed in the middle of the accessoryView. */ @property (nonatomic, strong) UILabel *editorTitle; /** The 'cancel' button displayed left in the accessoryView. */ @property (nonatomic, strong) UIButton *editorLeftButton; /** The 'accept' button displayed right in the accessoryView. */ @property (nonatomic, strong) UIButton *editorRightButton; /** The accessory view's maximum height. Default is 38 pts. */ @property (nonatomic, assign) CGFloat editorContentViewHeight; /** A Boolean value indicating whether the control is in edit mode. */ @property (nonatomic, getter = isEditing) BOOL editing; /** Verifies if the text can be edited. @param text The text to be edited. @return YES if the text is editable. */ - (BOOL)canEditText:(NSString *)text; /** Begins editing the text, by updating the 'editing' flag and the view constraints. */ - (void)beginTextEditing; /** End editing the text, by updating the 'editing' flag and the view constraints. */ - (void)endTextEdition; #pragma mark - Text Counting ///------------------------------------------------ /// @name Text Counting ///------------------------------------------------ /** The label used to display the character counts. */ @property (nonatomic, readonly) UILabel *charCountLabel; /** The maximum character count allowed. If larger than 0, a character count label will be displayed on top of the right button. Default is 0, which means limitless.*/ @property (nonatomic, readwrite) NSUInteger maxCharCount; /** The character counter formatting. Ignored if maxCharCount is 0. Default is None. */ @property (nonatomic, assign) SLKCounterStyle counterStyle; /** The character counter layout style. Ignored if maxCharCount is 0. Default is SLKCounterPositionTop. */ @property (nonatomic, assign) SLKCounterPosition counterPosition; /** YES if the maxmimum character count has been exceeded. */ @property (nonatomic, readonly) BOOL limitExceeded; /** The normal color used for character counter label. Default is lightGrayColor. */ @property (nonatomic, strong, readwrite) UIColor *charCountLabelNormalColor; /** The color used for character counter label when it has exceeded the limit. Default is redColor. */ @property (nonatomic, strong, readwrite) UIColor *charCountLabelWarningColor; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/SLKTextInputbar.m ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import "SLKTextInputbar.h" #import "SLKTextView.h" #import "SLKInputAccessoryView.h" #import "SLKTextView+SLKAdditions.h" #import "UIView+SLKAdditions.h" #import "SLKUIConstants.h" NSString * const SLKTextInputbarDidMoveNotification = @"SLKTextInputbarDidMoveNotification"; @interface SLKTextInputbar () @property (nonatomic, strong) NSLayoutConstraint *textViewBottomMarginC; @property (nonatomic, strong) NSLayoutConstraint *contentViewHC; @property (nonatomic, strong) NSLayoutConstraint *leftButtonWC; @property (nonatomic, strong) NSLayoutConstraint *leftButtonHC; @property (nonatomic, strong) NSLayoutConstraint *leftMarginWC; @property (nonatomic, strong) NSLayoutConstraint *leftButtonBottomMarginC; @property (nonatomic, strong) NSLayoutConstraint *rightButtonWC; @property (nonatomic, strong) NSLayoutConstraint *rightMarginWC; @property (nonatomic, strong) NSLayoutConstraint *rightButtonTopMarginC; @property (nonatomic, strong) NSLayoutConstraint *rightButtonBottomMarginC; @property (nonatomic, strong) NSLayoutConstraint *editorContentViewHC; @property (nonatomic, strong) NSArray *charCountLabelVCs; @property (nonatomic, strong) UILabel *charCountLabel; @property (nonatomic) CGPoint previousOrigin; @property (nonatomic, strong) Class textViewClass; @property (nonatomic, getter=isHidden) BOOL hidden; // Required override @end @implementation SLKTextInputbar @synthesize textView = _textView; @synthesize contentView = _contentView; @synthesize inputAccessoryView = _inputAccessoryView; @synthesize hidden = _hidden; #pragma mark - Initialization - (instancetype)initWithTextViewClass:(Class)textViewClass { if (self = [super init]) { self.textViewClass = textViewClass; [self slk_commonInit]; } return self; } - (id)init { if (self = [super init]) { [self slk_commonInit]; } return self; } - (instancetype)initWithCoder:(NSCoder *)coder { if (self = [super initWithCoder:coder]) { [self slk_commonInit]; } return self; } - (void)slk_commonInit { self.charCountLabelNormalColor = [UIColor lightGrayColor]; self.charCountLabelWarningColor = [UIColor redColor]; self.autoHideRightButton = YES; self.editorContentViewHeight = 38.0; self.contentInset = UIEdgeInsetsMake(5.0, 8.0, 5.0, 8.0); // Since iOS 11, it is required to call -layoutSubviews before adding custom subviews // so private UIToolbar subviews don't interfere on the touch hierarchy [self layoutSubviews]; [self addSubview:self.editorContentView]; [self addSubview:self.leftButton]; [self addSubview:self.rightButton]; [self addSubview:self.textView]; [self addSubview:self.charCountLabel]; [self addSubview:self.contentView]; [self slk_setupViewConstraints]; [self slk_updateConstraintConstants]; self.counterStyle = SLKCounterStyleNone; self.counterPosition = SLKCounterPositionTop; [self slk_registerNotifications]; [self slk_registerTo:self.layer forSelector:@selector(position)]; [self slk_registerTo:self.leftButton.imageView forSelector:@selector(image)]; [self slk_registerTo:self.rightButton.titleLabel forSelector:@selector(font)]; } #pragma mark - UIView Overrides - (void)layoutIfNeeded { if (self.constraints.count == 0 || !self.window) { return; } [self slk_updateConstraintConstants]; [super layoutIfNeeded]; } - (CGSize)intrinsicContentSize { return CGSizeMake(UIViewNoIntrinsicMetric, [self minimumInputbarHeight]); } + (BOOL)requiresConstraintBasedLayout { return YES; } #pragma mark - Getters - (SLKTextView *)textView { if (!_textView) { Class class = self.textViewClass ? : [SLKTextView class]; _textView = [[class alloc] init]; _textView.translatesAutoresizingMaskIntoConstraints = NO; _textView.font = [UIFont systemFontOfSize:15.0]; _textView.maxNumberOfLines = [self slk_defaultNumberOfLines]; _textView.keyboardType = UIKeyboardTypeTwitter; _textView.returnKeyType = UIReturnKeyDefault; _textView.enablesReturnKeyAutomatically = YES; _textView.scrollIndicatorInsets = UIEdgeInsetsMake(0.0, -1.0, 0.0, 1.0); _textView.textContainerInset = UIEdgeInsetsMake(8.0, 4.0, 8.0, 0.0); _textView.layer.cornerRadius = 5.0; _textView.layer.borderWidth = 0.5; _textView.layer.borderColor = [UIColor colorWithRed:200.0/255.0 green:200.0/255.0 blue:205.0/255.0 alpha:1.0].CGColor; } return _textView; } - (UIView *)contentView { if (!_contentView) { _contentView = [UIView new]; _contentView.translatesAutoresizingMaskIntoConstraints = NO; _contentView.backgroundColor = [UIColor clearColor]; _contentView.clipsToBounds = YES; } return _contentView; } - (SLKInputAccessoryView *)inputAccessoryView { if (!_inputAccessoryView) { _inputAccessoryView = [[SLKInputAccessoryView alloc] initWithFrame:CGRectZero]; _inputAccessoryView.backgroundColor = [UIColor clearColor]; _inputAccessoryView.userInteractionEnabled = NO; } return _inputAccessoryView; } - (UIButton *)leftButton { if (!_leftButton) { _leftButton = [UIButton buttonWithType:UIButtonTypeSystem]; _leftButton.translatesAutoresizingMaskIntoConstraints = NO; _leftButton.titleLabel.font = [UIFont systemFontOfSize:15.0]; } return _leftButton; } - (UIButton *)rightButton { if (!_rightButton) { _rightButton = [UIButton buttonWithType:UIButtonTypeSystem]; _rightButton.translatesAutoresizingMaskIntoConstraints = NO; _rightButton.titleLabel.font = [UIFont boldSystemFontOfSize:15.0]; _rightButton.enabled = NO; NSString *title = NSLocalizedString(@"Send", nil); [_rightButton setTitle:title forState:UIControlStateNormal]; } return _rightButton; } - (UIView *)editorContentView { if (!_editorContentView) { _editorContentView = [UIView new]; _editorContentView.translatesAutoresizingMaskIntoConstraints = NO; _editorContentView.backgroundColor = self.backgroundColor; _editorContentView.clipsToBounds = YES; _editorContentView.hidden = YES; [_editorContentView addSubview:self.editorTitle]; [_editorContentView addSubview:self.editorLeftButton]; [_editorContentView addSubview:self.editorRightButton]; NSDictionary *views = @{@"label": self.editorTitle, @"leftButton": self.editorLeftButton, @"rightButton": self.editorRightButton, }; NSDictionary *metrics = @{@"left" : @(self.contentInset.left), @"right" : @(self.contentInset.right) }; [_editorContentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(left)-[leftButton(60)]-(left)-[label(>=0)]-(right)-[rightButton(60)]-(<=right)-|" options:0 metrics:metrics views:views]]; [_editorContentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[leftButton]|" options:0 metrics:metrics views:views]]; [_editorContentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[rightButton]|" options:0 metrics:metrics views:views]]; [_editorContentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[label]|" options:0 metrics:metrics views:views]]; } return _editorContentView; } - (UILabel *)editorTitle { if (!_editorTitle) { _editorTitle = [UILabel new]; _editorTitle.translatesAutoresizingMaskIntoConstraints = NO; _editorTitle.textAlignment = NSTextAlignmentCenter; _editorTitle.backgroundColor = [UIColor clearColor]; _editorTitle.font = [UIFont boldSystemFontOfSize:15.0]; NSString *title = NSLocalizedString(@"Editing Message", nil); _editorTitle.text = title; } return _editorTitle; } - (UIButton *)editorLeftButton { if (!_editorLeftButton) { _editorLeftButton = [UIButton buttonWithType:UIButtonTypeSystem]; _editorLeftButton.translatesAutoresizingMaskIntoConstraints = NO; _editorLeftButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; _editorLeftButton.titleLabel.font = [UIFont systemFontOfSize:15.0]; NSString *title = NSLocalizedString(@"Cancel", nil); [_editorLeftButton setTitle:title forState:UIControlStateNormal]; } return _editorLeftButton; } - (UIButton *)editorRightButton { if (!_editorRightButton) { _editorRightButton = [UIButton buttonWithType:UIButtonTypeSystem]; _editorRightButton.translatesAutoresizingMaskIntoConstraints = NO; _editorRightButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight; _editorRightButton.titleLabel.font = [UIFont boldSystemFontOfSize:15.0]; _editorRightButton.enabled = NO; NSString *title = NSLocalizedString(@"Save", nil); [_editorRightButton setTitle:title forState:UIControlStateNormal]; } return _editorRightButton; } - (UILabel *)charCountLabel { if (!_charCountLabel) { _charCountLabel = [UILabel new]; _charCountLabel.translatesAutoresizingMaskIntoConstraints = NO; _charCountLabel.backgroundColor = [UIColor clearColor]; _charCountLabel.textAlignment = NSTextAlignmentRight; _charCountLabel.font = [UIFont systemFontOfSize:11.0]; _charCountLabel.hidden = NO; } return _charCountLabel; } - (BOOL)isHidden { return _hidden; } - (CGFloat)minimumInputbarHeight { CGFloat minimumHeight = self.textView.intrinsicContentSize.height; minimumHeight += self.contentInset.top; minimumHeight += self.slk_bottomMargin; return minimumHeight; } - (CGFloat)appropriateHeight { CGFloat height = 0.0; CGFloat minimumHeight = [self minimumInputbarHeight]; if (self.textView.numberOfLines == 1) { height = minimumHeight; } else if (self.textView.numberOfLines < self.textView.maxNumberOfLines) { height = [self slk_inputBarHeightForLines:self.textView.numberOfLines]; } else { height = [self slk_inputBarHeightForLines:self.textView.maxNumberOfLines]; } if (height < minimumHeight) { height = minimumHeight; } if (self.isEditing) { height += self.editorContentViewHeight; } return roundf(height); } - (BOOL)limitExceeded { NSString *text = [self.textView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (self.maxCharCount > 0 && text.length > self.maxCharCount) { return YES; } return NO; } - (CGFloat)slk_inputBarHeightForLines:(NSUInteger)numberOfLines { CGFloat height = self.textView.intrinsicContentSize.height; height -= self.textView.font.lineHeight; height += roundf(self.textView.font.lineHeight*numberOfLines); height += self.contentInset.top; height += self.slk_bottomMargin; return height; } - (CGFloat)slk_bottomMargin { CGFloat margin = self.contentInset.bottom; margin += self.slk_contentViewHeight; return margin; } - (CGFloat)slk_contentViewHeight { if (!self.editing) { return CGRectGetHeight(self.contentView.frame); } return 0.0; } - (CGFloat)slk_appropriateRightButtonWidth { if (self.autoHideRightButton) { if (self.textView.text.length == 0) { return 0.0; } } return [self.rightButton intrinsicContentSize].width; } - (CGFloat)slk_appropriateRightButtonMargin { if (self.autoHideRightButton) { if (self.textView.text.length == 0) { return 0.0; } } return self.contentInset.right; } - (NSUInteger)slk_defaultNumberOfLines { if (SLK_IS_IPAD) { return 8; } else if (SLK_IS_IPHONE4) { return 4; } else { return 6; } } #pragma mark - Setters - (void)setBackgroundColor:(UIColor *)color { self.barTintColor = color; self.editorContentView.backgroundColor = color; } - (void)setAutoHideRightButton:(BOOL)hide { if (self.autoHideRightButton == hide) { return; } _autoHideRightButton = hide; self.rightButtonWC.constant = [self slk_appropriateRightButtonWidth]; self.rightMarginWC.constant = [self slk_appropriateRightButtonMargin]; [self layoutIfNeeded]; } - (void)setContentInset:(UIEdgeInsets)insets { if (UIEdgeInsetsEqualToEdgeInsets(self.contentInset, insets)) { return; } if (UIEdgeInsetsEqualToEdgeInsets(self.contentInset, UIEdgeInsetsZero)) { _contentInset = insets; return; } _contentInset = insets; // Add new constraints [self removeConstraints:self.constraints]; [self slk_setupViewConstraints]; // Add constant values and refresh layout [self slk_updateConstraintConstants]; [super layoutIfNeeded]; } - (void)setEditing:(BOOL)editing { if (self.isEditing == editing) { return; } _editing = editing; _editorContentView.hidden = !editing; self.contentViewHC.active = editing; [super setNeedsLayout]; [super layoutIfNeeded]; } - (void)setHidden:(BOOL)hidden { // We don't call super here, since we want to avoid to visually hide the view. // The hidden render state is handled by the view controller. _hidden = hidden; if (!self.isEditing) { self.contentViewHC.active = hidden; [super setNeedsLayout]; [super layoutIfNeeded]; } } - (void)setCounterPosition:(SLKCounterPosition)counterPosition { if (self.counterPosition == counterPosition && self.charCountLabelVCs) { return; } // Clears the previous constraints if (_charCountLabelVCs.count > 0) { [self removeConstraints:_charCountLabelVCs]; _charCountLabelVCs = nil; } _counterPosition = counterPosition; NSDictionary *views = @{@"rightButton": self.rightButton, @"charCountLabel": self.charCountLabel }; NSDictionary *metrics = @{@"top" : @(self.contentInset.top), @"bottom" : @(-self.slk_bottomMargin/2.0) }; // Constraints are different depending of the counter's position type if (counterPosition == SLKCounterPositionBottom) { _charCountLabelVCs = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[charCountLabel]-(bottom)-[rightButton]" options:0 metrics:metrics views:views]; } else { _charCountLabelVCs = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(top@750)-[charCountLabel]-(>=0)-|" options:0 metrics:metrics views:views]; } [self addConstraints:self.charCountLabelVCs]; } #pragma mark - Text Editing - (BOOL)canEditText:(NSString *)text { if ((self.isEditing && [self.textView.text isEqualToString:text]) || self.isHidden) { return NO; } return YES; } - (void)beginTextEditing { if (self.isEditing || self.isHidden) { return; } self.editing = YES; [self slk_updateConstraintConstants]; if (!self.isFirstResponder) { [self layoutIfNeeded]; } } - (void)endTextEdition { if (!self.isEditing || self.isHidden) { return; } self.editing = NO; [self slk_updateConstraintConstants]; } #pragma mark - Character Counter - (void)slk_updateCounter { NSString *text = [self.textView.text stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; NSString *counter = nil; if (self.counterStyle == SLKCounterStyleNone) { counter = [NSString stringWithFormat:@"%lu", (unsigned long)text.length]; } if (self.counterStyle == SLKCounterStyleSplit) { counter = [NSString stringWithFormat:@"%lu/%lu", (unsigned long)text.length, (unsigned long)self.maxCharCount]; } if (self.counterStyle == SLKCounterStyleCountdown) { counter = [NSString stringWithFormat:@"%ld", (long)(text.length - self.maxCharCount)]; } if (self.counterStyle == SLKCounterStyleCountdownReversed) { counter = [NSString stringWithFormat:@"%ld", (long)(self.maxCharCount - text.length)]; } self.charCountLabel.text = counter; self.charCountLabel.textColor = [self limitExceeded] ? self.charCountLabelWarningColor : self.charCountLabelNormalColor; } #pragma mark - Notification Events - (void)slk_didChangeTextViewText:(NSNotification *)notification { SLKTextView *textView = (SLKTextView *)notification.object; // Skips this it's not the expected textView. if (![textView isEqual:self.textView]) { return; } // Updates the char counter label if (self.maxCharCount > 0) { [self slk_updateCounter]; } if (self.autoHideRightButton && !self.isEditing) { CGFloat rightButtonNewWidth = [self slk_appropriateRightButtonWidth]; // Only updates if the width did change if (self.rightButtonWC.constant == rightButtonNewWidth) { return; } self.rightButtonWC.constant = rightButtonNewWidth; self.rightMarginWC.constant = [self slk_appropriateRightButtonMargin]; [self.rightButton layoutIfNeeded]; // Avoids the right button to stretch when animating the constraint changes BOOL bounces = self.bounces && [self.textView isFirstResponder]; if (self.window) { [self slk_animateLayoutIfNeededWithBounce:bounces options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction animations:NULL]; } else { [self layoutIfNeeded]; } } } - (void)slk_didChangeTextViewContentSize:(NSNotification *)notification { if (self.maxCharCount > 0) { BOOL shouldHide = (self.textView.numberOfLines == 1) || self.editing; self.charCountLabel.hidden = shouldHide; } } - (void)slk_didChangeContentSizeCategory:(NSNotification *)notification { if (!self.textView.isDynamicTypeEnabled) { return; } [self layoutIfNeeded]; } #pragma mark - View Auto-Layout - (void)slk_setupViewConstraints { NSDictionary *views = @{@"textView": self.textView, @"leftButton": self.leftButton, @"rightButton": self.rightButton, @"editorContentView": self.editorContentView, @"charCountLabel": self.charCountLabel, @"contentView": self.contentView, }; NSDictionary *metrics = @{@"top" : @(self.contentInset.top), @"left" : @(self.contentInset.left), @"right" : @(self.contentInset.right), }; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(left)-[leftButton(0)]-(<=left)-[textView]-(right)-[rightButton(0)]-(right)-|" options:0 metrics:metrics views:views]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=0)-[leftButton(0)]-(0@750)-|" options:0 metrics:metrics views:views]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=0)-[rightButton]-(<=0)-|" options:0 metrics:metrics views:views]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(left@250)-[charCountLabel(<=50@1000)]-(right@750)-|" options:0 metrics:metrics views:views]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[editorContentView(0)]-(<=top)-[textView(0@999)]-(0)-|" options:0 metrics:metrics views:views]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[editorContentView]|" options:0 metrics:metrics views:views]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options:0 metrics:metrics views:views]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[contentView(0)]|" options:0 metrics:metrics views:views]]; self.textViewBottomMarginC = [self slk_constraintForAttribute:NSLayoutAttributeBottom firstItem:self secondItem:self.textView]; self.editorContentViewHC = [self slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.editorContentView secondItem:nil]; self.contentViewHC = [self slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.contentView secondItem:nil];; self.contentViewHC.active = NO; // Disabled by default, so the height is calculated with the height of its subviews self.leftButtonWC = [self slk_constraintForAttribute:NSLayoutAttributeWidth firstItem:self.leftButton secondItem:nil]; self.leftButtonHC = [self slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.leftButton secondItem:nil]; self.leftButtonBottomMarginC = [self slk_constraintForAttribute:NSLayoutAttributeBottom firstItem:self secondItem:self.leftButton]; self.leftMarginWC = [[self slk_constraintsForAttribute:NSLayoutAttributeLeading] firstObject]; self.rightButtonWC = [self slk_constraintForAttribute:NSLayoutAttributeWidth firstItem:self.rightButton secondItem:nil]; self.rightMarginWC = [[self slk_constraintsForAttribute:NSLayoutAttributeTrailing] firstObject]; self.rightButtonTopMarginC = [self slk_constraintForAttribute:NSLayoutAttributeTop firstItem:self.rightButton secondItem:self]; self.rightButtonBottomMarginC = [self slk_constraintForAttribute:NSLayoutAttributeBottom firstItem:self secondItem:self.rightButton]; } - (void)slk_updateConstraintConstants { CGFloat zero = 0.0; self.textViewBottomMarginC.constant = self.slk_bottomMargin; if (self.isEditing) { self.editorContentViewHC.constant = self.editorContentViewHeight; self.leftButtonWC.constant = zero; self.leftButtonHC.constant = zero; self.leftMarginWC.constant = zero; self.leftButtonBottomMarginC.constant = zero; self.rightButtonWC.constant = zero; self.rightMarginWC.constant = zero; } else { self.editorContentViewHC.constant = zero; CGSize leftButtonSize = [self.leftButton imageForState:self.leftButton.state].size; if (leftButtonSize.width > 0) { self.leftButtonHC.constant = roundf(leftButtonSize.height); self.leftButtonBottomMarginC.constant = roundf((self.intrinsicContentSize.height - leftButtonSize.height) / 2.0) + self.slk_contentViewHeight / 2.0; } self.leftButtonWC.constant = roundf(leftButtonSize.width); self.leftMarginWC.constant = (leftButtonSize.width > 0) ? self.contentInset.left : zero; self.rightButtonWC.constant = [self slk_appropriateRightButtonWidth]; self.rightMarginWC.constant = [self slk_appropriateRightButtonMargin]; CGFloat rightVerMargin = (self.intrinsicContentSize.height - self.slk_contentViewHeight - self.rightButton.intrinsicContentSize.height) / 2.0; CGFloat rightVerBottomMargin = rightVerMargin + self.slk_contentViewHeight; self.rightButtonTopMarginC.constant = rightVerMargin; self.rightButtonBottomMarginC.constant = rightVerBottomMargin; } } #pragma mark - Observers - (void)slk_registerTo:(id)object forSelector:(SEL)selector { if (object) { [object addObserver:self forKeyPath:NSStringFromSelector(selector) options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; } } - (void)slk_unregisterFrom:(id)object forSelector:(SEL)selector { if (object) { [object removeObserver:self forKeyPath:NSStringFromSelector(selector)]; } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([object isEqual:self.layer] && [keyPath isEqualToString:NSStringFromSelector(@selector(position))]) { if (!CGPointEqualToPoint(self.previousOrigin, self.frame.origin)) { self.previousOrigin = self.frame.origin; [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextInputbarDidMoveNotification object:self userInfo:@{@"origin": [NSValue valueWithCGPoint:self.previousOrigin]}]; } } else if ([object isEqual:self.leftButton.imageView] && [keyPath isEqualToString:NSStringFromSelector(@selector(image))]) { UIImage *newImage = change[NSKeyValueChangeNewKey]; UIImage *oldImage = change[NSKeyValueChangeOldKey]; if (![newImage isEqual:oldImage]) { [self slk_updateConstraintConstants]; } } else if ([object isEqual:self.rightButton.titleLabel] && [keyPath isEqualToString:NSStringFromSelector(@selector(font))]) { [self slk_updateConstraintConstants]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } #pragma mark - NSNotificationCenter registration - (void)slk_registerNotifications { [self slk_unregisterNotifications]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeTextViewText:) name:UITextViewTextDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeTextViewContentSize:) name:SLKTextViewContentSizeDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeContentSizeCategory:) name:UIContentSizeCategoryDidChangeNotification object:nil]; } - (void)slk_unregisterNotifications { [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:SLKTextViewContentSizeDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIContentSizeCategoryDidChangeNotification object:nil]; } #pragma mark - Lifeterm - (void)dealloc { [self slk_unregisterNotifications]; [self slk_unregisterFrom:self.layer forSelector:@selector(position)]; [self slk_unregisterFrom:self.leftButton.imageView forSelector:@selector(image)]; [self slk_unregisterFrom:self.rightButton.titleLabel forSelector:@selector(font)]; } @end ================================================ FILE: Source/SLKTextView+SLKAdditions.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import "SLKTextView.h" NS_ASSUME_NONNULL_BEGIN /** @name SLKTextView additional features used for SlackTextViewController. */ @interface SLKTextView (SLKAdditions) /** Clears the text. @param clearUndo YES if clearing the text should also clear the undo manager (if enabled). */ - (void)slk_clearText:(BOOL)clearUndo; /** Scrolls to the very end of the content size, animated. @param animated YES if the scrolling should be animated. */ - (void)slk_scrollToBottomAnimated:(BOOL)animated; /** Scrolls to the caret position, animated. @param animated YES if the scrolling should be animated. */ - (void)slk_scrollToCaretPositonAnimated:(BOOL)animated; /** Inserts a line break at the caret's position. */ - (void)slk_insertNewLineBreak; /** Inserts a string at the caret's position. @param text The string to be appended to the current text. */ - (void)slk_insertTextAtCaretRange:(NSString *)text; /** Insert a string at the caret's position with stylization from the attributes. @param text The string to be appended to the current text. @param attributes The attributes used to stylize the text. */ - (void)slk_insertTextAtCaretRange:(NSString *)text withAttributes:(NSDictionary *)attributes; /** Adds a string to a specific range. @param text The string to be appended to the current text. @param range The range where to insert text. @return The range of the newly inserted text. */ - (NSRange)slk_insertText:(NSString *)text inRange:(NSRange)range; /** Adds a string to a specific range, with stylization from the attributes. @param text The string to be appended to the current text. @param attributes The attributes used to stylize the text. @param range The range where to insert text. @return The range of the newly inserted text. */ - (NSRange)slk_insertText:(NSString *)text withAttributes:(NSDictionary *)attributes inRange:(NSRange)range; /** Sets the text attributes for the attributed string in the provided range. @param attributes The attributes used to style NSAttributedString class. @param range The range of the text that needs to be stylized by the given attributes. @return An attributed string. */ - (NSAttributedString *)slk_setAttributes:(NSDictionary *)attributes inRange:(NSRange)range; /** Inserts an attributed string at the caret's position. @param attributedText The attributed string to be appended. */ - (void)slk_insertAttributedTextAtCaretRange:(NSAttributedString *)attributedText; /** Adds an attributed string to a specific range. @param text The string to be appended to the current text. @param range The range where to insert text. @return The range of the newly inserted text. */ - (NSRange)slk_insertAttributedText:(NSAttributedString *)attributedText inRange:(NSRange)range; /** Removes all attributed string attributes from the text view, for the given range. @param range The range to remove the attributes. */ - (void)slk_clearAllAttributesInRange:(NSRange)range; /** Returns a default attributed string, using the text view's font and text color. @param text The string to be used for creating a new attributed string. @return An attributed string. */ - (NSAttributedString *)slk_defaultAttributedStringForText:(NSString *)text; /** Registers the current text for future undo actions. @param description A simple description associated with the Undo or Redo command. */ - (void)slk_prepareForUndo:(NSString *)description; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/SLKTextView+SLKAdditions.m ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import "SLKTextView+SLKAdditions.h" @implementation SLKTextView (SLKAdditions) - (void)slk_clearText:(BOOL)clearUndo { // Important to call self implementation, as SLKTextView overrides setText: to add additional features. [self setAttributedText:nil]; if (self.undoManagerEnabled && clearUndo) { [self.undoManager removeAllActions]; } } - (void)slk_scrollToCaretPositonAnimated:(BOOL)animated { if (animated) { [self scrollRangeToVisible:self.selectedRange]; } else { [UIView performWithoutAnimation:^{ [self scrollRangeToVisible:self.selectedRange]; }]; } } - (void)slk_scrollToBottomAnimated:(BOOL)animated { CGRect rect = [self caretRectForPosition:self.selectedTextRange.end]; rect.size.height += self.textContainerInset.bottom; if (animated) { [self scrollRectToVisible:rect animated:animated]; } else { [UIView performWithoutAnimation:^{ [self scrollRectToVisible:rect animated:NO]; }]; } } - (void)slk_insertNewLineBreak { [self slk_insertTextAtCaretRange:@"\n"]; // if the text view cannot expand anymore, scrolling to bottom are not animated to fix a UITextView issue scrolling twice. BOOL animated = !self.isExpanding; //Detected break. Should scroll to bottom if needed. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0125 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self slk_scrollToBottomAnimated:animated]; }); } - (void)slk_insertTextAtCaretRange:(NSString *)text { NSRange range = [self slk_insertText:text inRange:self.selectedRange]; self.selectedRange = NSMakeRange(range.location, 0); } - (void)slk_insertTextAtCaretRange:(NSString *)text withAttributes:(NSDictionary *)attributes { NSRange range = [self slk_insertText:text withAttributes:attributes inRange:self.selectedRange]; self.selectedRange = NSMakeRange(range.location, 0); } - (NSRange)slk_insertText:(NSString *)text inRange:(NSRange)range { NSAttributedString *attributedText = [self slk_defaultAttributedStringForText:text]; return [self slk_insertAttributedText:attributedText inRange:range]; } - (NSRange)slk_insertText:(NSString *)text withAttributes:(NSDictionary *)attributes inRange:(NSRange)range { NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:attributes]; return [self slk_insertAttributedText:attributedText inRange:range]; } - (NSAttributedString *)slk_setAttributes:(NSDictionary *)attributes inRange:(NSRange)range { NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText]; [attributedText setAttributes:attributes range:range]; [self setAttributedText:attributedText]; return self.attributedText; } - (void)slk_insertAttributedTextAtCaretRange:(NSAttributedString *)attributedText { NSRange range = [self slk_insertAttributedText:attributedText inRange:self.selectedRange]; self.selectedRange = NSMakeRange(range.location, 0); } - (NSRange)slk_insertAttributedText:(NSAttributedString *)attributedText inRange:(NSRange)range { // Skip if the attributed text is empty if (attributedText.length == 0) { return NSMakeRange(0, 0); } // Registers for undo management [self slk_prepareForUndo:@"Attributed text appending"]; // Append the new string at the caret position if (range.length == 0) { NSAttributedString *leftAttributedString = [self.attributedText attributedSubstringFromRange:NSMakeRange(0, range.location)]; NSAttributedString *rightAttributedString = [self.attributedText attributedSubstringFromRange:NSMakeRange(range.location, self.attributedText.length-range.location)]; NSMutableAttributedString *newAttributedText = [NSMutableAttributedString new]; [newAttributedText appendAttributedString:leftAttributedString]; [newAttributedText appendAttributedString:attributedText]; [newAttributedText appendAttributedString:rightAttributedString]; [self setAttributedText:newAttributedText]; range.location += attributedText.length; return range; } // Some text is selected, so we replace it with the new text else if (range.location != NSNotFound && range.length > 0) { NSMutableAttributedString *mutableAttributeText = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText]; [mutableAttributeText replaceCharactersInRange:range withAttributedString:attributedText]; [self setAttributedText:mutableAttributeText]; range.location += self.attributedText.length; return range; } // No text has been inserted, but still return the caret range return self.selectedRange; } - (void)slk_clearAllAttributesInRange:(NSRange)range { NSMutableAttributedString *mutableAttributedText = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText]; [mutableAttributedText setAttributes:nil range:range]; [self setAttributedText:mutableAttributedText]; } - (NSAttributedString *)slk_defaultAttributedStringForText:(NSString *)text { NSMutableDictionary *attributes = [NSMutableDictionary dictionary]; if (self.textColor) { attributes[NSForegroundColorAttributeName] = self.textColor; } if (self.font) { attributes[NSFontAttributeName] = self.font; } return [[NSAttributedString alloc] initWithString:text attributes:attributes]; } - (void)slk_prepareForUndo:(NSString *)description { if (!self.undoManagerEnabled) { return; } SLKTextView *prepareInvocation = [self.undoManager prepareWithInvocationTarget:self]; [prepareInvocation setText:self.text]; [self.undoManager setActionName:description]; } @end ================================================ FILE: Source/SLKTextView.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import #import "SLKTextInput.h" typedef NS_OPTIONS(NSUInteger, SLKPastableMediaType) { SLKPastableMediaTypeNone = 0, SLKPastableMediaTypePNG = 1 << 0, SLKPastableMediaTypeJPEG = 1 << 1, SLKPastableMediaTypeTIFF = 1 << 2, SLKPastableMediaTypeGIF = 1 << 3, SLKPastableMediaTypeMOV = 1 << 4, SLKPastableMediaTypePassbook = 1 << 5, SLKPastableMediaTypeImages = SLKPastableMediaTypePNG|SLKPastableMediaTypeJPEG|SLKPastableMediaTypeTIFF|SLKPastableMediaTypeGIF, SLKPastableMediaTypeVideos = SLKPastableMediaTypeMOV, SLKPastableMediaTypeAll = SLKPastableMediaTypeImages|SLKPastableMediaTypeMOV }; NS_ASSUME_NONNULL_BEGIN UIKIT_EXTERN NSString * const SLKTextViewTextWillChangeNotification; UIKIT_EXTERN NSString * const SLKTextViewContentSizeDidChangeNotification; UIKIT_EXTERN NSString * const SLKTextViewSelectedRangeDidChangeNotification; UIKIT_EXTERN NSString * const SLKTextViewDidPasteItemNotification; UIKIT_EXTERN NSString * const SLKTextViewDidShakeNotification; UIKIT_EXTERN NSString * const SLKTextViewPastedItemContentType; UIKIT_EXTERN NSString * const SLKTextViewPastedItemMediaType; UIKIT_EXTERN NSString * const SLKTextViewPastedItemData; @protocol SLKTextViewDelegate; /** @name A custom text input view. */ @interface SLKTextView : UITextView @property (nonatomic, weak) iddelegate; /** The placeholder text string. Default is nil. */ @property (nonatomic, copy) NSString *_Nullable placeholder; /** The placeholder color. Default is lightGrayColor. */ @property (nonatomic, copy) UIColor *_Null_unspecified placeholderColor; /** The placeholder's number of lines. Default is 1. */ @property (nonatomic, readwrite) NSInteger placeholderNumberOfLines; /** The placeholder's font. Default is the textView's font. */ @property (nonatomic, copy, null_resettable) UIFont *placeholderFont; /** The maximum number of lines before enabling scrolling. Default is 0 wich means limitless. If dynamic type is enabled, the maximum number of lines will be calculated proportionally to the user preferred font size. */ @property (nonatomic, readwrite) NSUInteger maxNumberOfLines; /** The current displayed number of lines. */ @property (nonatomic, readonly) NSUInteger numberOfLines; /** The supported media types allowed to be pasted in the text view, such as images or videos. Default is None. */ @property (nonatomic) SLKPastableMediaType pastableMediaTypes; /** YES if the text view is and can still expand it self, depending if the maximum number of lines are reached. */ @property (nonatomic, readonly) BOOL isExpanding; /** YES if quickly refreshed the textview without the intension to dismiss the keyboard. @view -disableQuicktypeBar: for more details. */ @property (nonatomic, readwrite) BOOL didNotResignFirstResponder; /** YES if the magnifying glass is visible. This feature is deprecated since there are no legit alternatives to detect the magnifying glass. Open Radar: http://openradar.appspot.com/radar?id=5021485877952512 */ @property (nonatomic, getter=isLoupeVisible) BOOL loupeVisible DEPRECATED_ATTRIBUTE; /** YES if the keyboard track pad has been recognized. iOS 9 only. */ @property (nonatomic, readonly, getter=isTrackpadEnabled) BOOL trackpadEnabled; /** YES if autocorrection and spell checking are enabled. On iOS8, this property also controls the predictive QuickType bar from being visible. Default is YES. */ @property (nonatomic, getter=isTypingSuggestionEnabled) BOOL typingSuggestionEnabled; /** YES if the text view supports undoing, either using UIMenuController, or with ctrl+z when using an external keyboard. Default is YES. */ @property (nonatomic, readwrite) BOOL undoManagerEnabled; /** YES if the font size should dynamically adapt based on the font sizing option preferred by the user. Default is YES. */ @property (nonatomic, getter=isDynamicTypeEnabled) BOOL dynamicTypeEnabled; /** Some text view properties don't update when it's already firstResponder (auto-correction, spelling-check, etc.) To be able to update the text view while still being first responder, requieres to switch quickly from -resignFirstResponder to -becomeFirstResponder. When doing so, the flag 'didNotResignFirstResponder' is momentarly set to YES before it goes back to -isFirstResponder, to be able to prevent some tasks to be excuted because of UIKeyboard notifications. You can also use this method to confirm an auto-correction programatically, before the text view resigns first responder. */ - (void)refreshFirstResponder; - (void)refreshInputViews; /** Notifies the text view that the user pressed any arrow key. This is used to move the cursor up and down while having multiple lines. */ - (void)didPressArrowKey:(UIKeyCommand *)keyCommand; #pragma mark - Markdown Formatting /** YES if the a markdown closure symbol should be added automatically after double spacebar tap, just like the native gesture to add a sentence period. Default is YES. This will always be NO if there isn't any registered formatting symbols. */ @property (nonatomic, readonly, getter=isFormattingEnabled) BOOL formattingEnabled; /** An array of the registered formatting symbols. */ @property (nonatomic, readonly) NSArray *_Nullable registeredSymbols; /** Registers any string markdown symbol for formatting tooltip, presented after selecting some text. The symbol must be valid string (i.e: '*', '~', '_', and so on). This also checks if no repeated symbols are inserted, and respects the ordering for the tooltip. @param symbol A markdown symbol to be prefixed and sufixed to a text selection. @param title The tooltip item title for this formatting. */ - (void)registerMarkdownFormattingSymbol:(NSString *)symbol withTitle:(NSString *)title; #pragma mark - External Keyboard Support /** Registers and observes key commands' updates, when the text view is first responder. Instead of typically overriding UIResponder's -keyCommands method, it is better to use this API for easier and safer implementation of key input detection. @param input The keys that must be pressed by the user. Required. @param modifiers The bit mask of modifier keys that must be pressed. Use 0 if none. @param title The title to display to the user. Optional. @param completion A completion block called whenever the key combination is detected. Required. */ - (void)observeKeyInput:(NSString *)input modifiers:(UIKeyModifierFlags)modifiers title:(NSString *_Nullable)title completion:(void (^)(UIKeyCommand *keyCommand))completion; @end @protocol SLKTextViewDelegate @optional /** Asks the delegate whether the specified formatting symbol should be displayed in the tooltip. This is useful to remove some tooltip options when they no longer apply in some context. For example, Blockquotes formatting requires the symbol to be prefixed at the begining of a paragraph. @param textView The text view containing the changes. @param symbol The formatting symbol to be verified. @return YES if the formatting symbol should be displayed in the tooltip. Default is YES. */ - (BOOL)textView:(SLKTextView *)textView shouldOfferFormattingForSymbol:(NSString *)symbol; /** Asks the delegate whether the specified formatting symbol should be suffixed, to close the formatting wrap. @para The prefix range */ - (BOOL)textView:(SLKTextView *)textView shouldInsertSuffixForFormattingWithSymbol:(NSString *)symbol prefixRange:(NSRange)prefixRange; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/SLKTextView.m ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import "SLKTextView.h" #import "SLKTextView+SLKAdditions.h" #import "SLKUIConstants.h" NSString * const SLKTextViewTextWillChangeNotification = @"SLKTextViewTextWillChangeNotification"; NSString * const SLKTextViewContentSizeDidChangeNotification = @"SLKTextViewContentSizeDidChangeNotification"; NSString * const SLKTextViewSelectedRangeDidChangeNotification = @"SLKTextViewSelectedRangeDidChangeNotification"; NSString * const SLKTextViewDidPasteItemNotification = @"SLKTextViewDidPasteItemNotification"; NSString * const SLKTextViewDidShakeNotification = @"SLKTextViewDidShakeNotification"; NSString * const SLKTextViewPastedItemContentType = @"SLKTextViewPastedItemContentType"; NSString * const SLKTextViewPastedItemMediaType = @"SLKTextViewPastedItemMediaType"; NSString * const SLKTextViewPastedItemData = @"SLKTextViewPastedItemData"; static NSString *const SLKTextViewGenericFormattingSelectorPrefix = @"slk_format_"; @interface SLKTextView () // The label used as placeholder @property (nonatomic, strong) UILabel *placeholderLabel; // The initial font point size, used for dynamic type calculations @property (nonatomic) CGFloat initialFontSize; // Used for moving the caret up/down @property (nonatomic) UITextLayoutDirection verticalMoveDirection; @property (nonatomic) CGRect verticalMoveStartCaretRect; @property (nonatomic) CGRect verticalMoveLastCaretRect; // Used for detecting if the scroll indicator was previously flashed @property (nonatomic) BOOL didFlashScrollIndicators; @property (nonatomic, strong) NSMutableArray *registeredFormattingTitles; @property (nonatomic, strong) NSMutableArray *registeredFormattingSymbols; @property (nonatomic, getter=isFormatting) BOOL formatting; // The keyboard commands available for external keyboards @property (nonatomic, strong) NSMutableDictionary *registeredKeyCommands; @property (nonatomic, strong) NSMutableDictionary *registeredKeyCallbacks; @end @implementation SLKTextView @dynamic delegate; #pragma mark - Initialization - (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer { if (self = [super initWithFrame:frame textContainer:textContainer]) { [self slk_commonInit]; } return self; } - (instancetype)initWithCoder:(NSCoder *)coder { if (self = [super initWithCoder:coder]) { [self slk_commonInit]; } return self; } - (void)slk_commonInit { _pastableMediaTypes = SLKPastableMediaTypeNone; _dynamicTypeEnabled = YES; self.undoManagerEnabled = YES; self.editable = YES; self.selectable = YES; self.scrollEnabled = YES; self.scrollsToTop = NO; self.directionalLockEnabled = YES; self.dataDetectorTypes = UIDataDetectorTypeNone; [self slk_registerNotifications]; [self addObserver:self forKeyPath:NSStringFromSelector(@selector(contentSize)) options:NSKeyValueObservingOptionNew context:NULL]; } #pragma mark - UIView Overrides - (CGSize)intrinsicContentSize { CGFloat height = self.font.lineHeight; height += self.textContainerInset.top + self.textContainerInset.bottom; return CGSizeMake(UIViewNoIntrinsicMetric, height); } + (BOOL)requiresConstraintBasedLayout { return YES; } - (void)layoutIfNeeded { if (!self.window) { return; } [super layoutIfNeeded]; } - (void)layoutSubviews { [super layoutSubviews]; self.placeholderLabel.hidden = [self slk_shouldHidePlaceholder]; if (!self.placeholderLabel.hidden) { [UIView performWithoutAnimation:^{ self.placeholderLabel.frame = [self slk_placeholderRectThatFits:self.bounds]; [self sendSubviewToBack:self.placeholderLabel]; }]; } } #pragma mark - Getters - (UILabel *)placeholderLabel { if (!_placeholderLabel) { _placeholderLabel = [UILabel new]; _placeholderLabel.clipsToBounds = NO; _placeholderLabel.numberOfLines = 1; _placeholderLabel.autoresizesSubviews = NO; _placeholderLabel.font = self.font; _placeholderLabel.backgroundColor = [UIColor clearColor]; _placeholderLabel.textColor = [UIColor lightGrayColor]; _placeholderLabel.hidden = YES; _placeholderLabel.isAccessibilityElement = NO; [self addSubview:_placeholderLabel]; } return _placeholderLabel; } - (NSString *)placeholder { return self.placeholderLabel.text; } - (UIColor *)placeholderColor { return self.placeholderLabel.textColor; } - (UIFont *)placeholderFont { return self.placeholderLabel.font; } - (NSUInteger)numberOfLines { CGSize contentSize = self.contentSize; CGFloat contentHeight = contentSize.height; contentHeight -= self.textContainerInset.top + self.textContainerInset.bottom; NSUInteger lines = fabs(contentHeight/self.font.lineHeight); // This helps preventing the content's height to be larger that the bounds' height // Avoiding this way to have unnecessary scrolling in the text view when there is only 1 line of content if (lines == 1 && contentSize.height > self.bounds.size.height) { contentSize.height = self.bounds.size.height; self.contentSize = contentSize; } // Let's fallback to the minimum line count if (lines == 0) { lines = 1; } return lines; } - (NSUInteger)maxNumberOfLines { NSUInteger numberOfLines = _maxNumberOfLines; if (SLK_IS_LANDSCAPE) { if ((SLK_IS_IPHONE4 || SLK_IS_IPHONE5)) { numberOfLines = 2.0; // 2 lines max on smaller iPhones } else if (SLK_IS_IPHONE) { numberOfLines /= 2.0; // Half size on larger iPhone } } if (self.isDynamicTypeEnabled) { NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; CGFloat pointSizeDifference = SLKPointSizeDifferenceForCategory(contentSizeCategory); CGFloat factor = pointSizeDifference/self.initialFontSize; if (fabs(factor) > 0.75) { factor = 0.75; } numberOfLines -= floorf(numberOfLines * factor); // Calculates a dynamic number of lines depending of the user preferred font size } return numberOfLines; } - (BOOL)isTypingSuggestionEnabled { return (self.autocorrectionType == UITextAutocorrectionTypeNo) ? NO : YES; } - (BOOL)isFormattingEnabled { return (self.registeredFormattingSymbols.count > 0) ? YES : NO; } // Returns only a supported pasted item - (id)slk_pastedItem { NSString *contentType = [self slk_pasteboardContentType]; NSData *data = [[UIPasteboard generalPasteboard] dataForPasteboardType:contentType]; if (data && [data isKindOfClass:[NSData class]]) { SLKPastableMediaType mediaType = SLKPastableMediaTypeFromNSString(contentType); NSDictionary *userInfo = @{SLKTextViewPastedItemContentType: contentType, SLKTextViewPastedItemMediaType: @(mediaType), SLKTextViewPastedItemData: data}; return userInfo; } if ([[UIPasteboard generalPasteboard] URL]) { return [[[UIPasteboard generalPasteboard] URL] absoluteString]; } if ([[UIPasteboard generalPasteboard] string]) { return [[UIPasteboard generalPasteboard] string]; } return nil; } // Checks if any supported media found in the general pasteboard - (BOOL)slk_isPasteboardItemSupported { if ([self slk_pasteboardContentType].length > 0) { return YES; } return NO; } - (NSString *)slk_pasteboardContentType { NSArray *pasteboardTypes = [[UIPasteboard generalPasteboard] pasteboardTypes]; NSMutableArray *subpredicates = [NSMutableArray new]; for (NSString *type in [self slk_supportedMediaTypes]) { [subpredicates addObject:[NSPredicate predicateWithFormat:@"SELF == %@", type]]; } return [[pasteboardTypes filteredArrayUsingPredicate:[NSCompoundPredicate orPredicateWithSubpredicates:subpredicates]] firstObject]; } - (NSArray *)slk_supportedMediaTypes { if (self.pastableMediaTypes == SLKPastableMediaTypeNone) { return nil; } NSMutableArray *types = [NSMutableArray new]; if (self.pastableMediaTypes & SLKPastableMediaTypePNG) { [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypePNG)]; } if (self.pastableMediaTypes & SLKPastableMediaTypeJPEG) { [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeJPEG)]; } if (self.pastableMediaTypes & SLKPastableMediaTypeTIFF) { [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeTIFF)]; } if (self.pastableMediaTypes & SLKPastableMediaTypeGIF) { [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeGIF)]; } if (self.pastableMediaTypes & SLKPastableMediaTypeMOV) { [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeMOV)]; } if (self.pastableMediaTypes & SLKPastableMediaTypePassbook) { [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypePassbook)]; } if (self.pastableMediaTypes & SLKPastableMediaTypeImages) { [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeImages)]; } return types; } NSString *NSStringFromSLKPastableMediaType(SLKPastableMediaType type) { if (type == SLKPastableMediaTypePNG) { return @"public.png"; } if (type == SLKPastableMediaTypeJPEG) { return @"public.jpeg"; } if (type == SLKPastableMediaTypeTIFF) { return @"public.tiff"; } if (type == SLKPastableMediaTypeGIF) { return @"com.compuserve.gif"; } if (type == SLKPastableMediaTypeMOV) { return @"com.apple.quicktime"; } if (type == SLKPastableMediaTypePassbook) { return @"com.apple.pkpass"; } if (type == SLKPastableMediaTypeImages) { return @"com.apple.uikit.image"; } return nil; } SLKPastableMediaType SLKPastableMediaTypeFromNSString(NSString *string) { if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypePNG)]) { return SLKPastableMediaTypePNG; } if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeJPEG)]) { return SLKPastableMediaTypeJPEG; } if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeTIFF)]) { return SLKPastableMediaTypeTIFF; } if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeGIF)]) { return SLKPastableMediaTypeGIF; } if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeMOV)]) { return SLKPastableMediaTypeMOV; } if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypePassbook)]) { return SLKPastableMediaTypePassbook; } if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeImages)]) { return SLKPastableMediaTypeImages; } return SLKPastableMediaTypeNone; } - (BOOL)isExpanding { if (self.numberOfLines >= self.maxNumberOfLines) { return YES; } return NO; } - (BOOL)slk_shouldHidePlaceholder { if (self.placeholder.length == 0 || self.text.length > 0) { return YES; } return NO; } - (CGRect)slk_placeholderRectThatFits:(CGRect)bounds { CGFloat padding = self.textContainer.lineFragmentPadding; CGRect rect = CGRectZero; rect.size.height = [self.placeholderLabel sizeThatFits:bounds.size].height; rect.size.width = self.textContainer.size.width - padding*2.0; rect.origin = UIEdgeInsetsInsetRect(bounds, self.textContainerInset).origin; rect.origin.x += padding; return rect; } #pragma mark - Setters - (void)setPlaceholder:(NSString *)placeholder { self.placeholderLabel.text = placeholder; self.accessibilityLabel = placeholder; [self setNeedsLayout]; } - (void)setPlaceholderColor:(UIColor *)color { self.placeholderLabel.textColor = color; } - (void)setPlaceholderNumberOfLines:(NSInteger)numberOfLines { self.placeholderLabel.numberOfLines = numberOfLines; [self setNeedsLayout]; } - (void)setPlaceholderFont:(UIFont *)placeholderFont { if (!placeholderFont) { self.placeholderLabel.font = self.font; } else { self.placeholderLabel.font = placeholderFont; } } - (void)setUndoManagerEnabled:(BOOL)enabled { if (self.undoManagerEnabled == enabled) { return; } self.undoManager.levelsOfUndo = 10; [self.undoManager removeAllActions]; [self.undoManager setActionIsDiscardable:YES]; _undoManagerEnabled = enabled; } - (void)setTypingSuggestionEnabled:(BOOL)enabled { if (self.isTypingSuggestionEnabled == enabled) { return; } self.autocorrectionType = enabled ? UITextAutocorrectionTypeDefault : UITextAutocorrectionTypeNo; self.spellCheckingType = enabled ? UITextSpellCheckingTypeDefault : UITextSpellCheckingTypeNo; [self refreshFirstResponder]; } - (void)setContentOffset:(CGPoint)contentOffset { // At times during a layout pass, the content offset's x value may change. // Since we only care about vertical offset, let's override its horizontal value to avoid other layout issues. [super setContentOffset:CGPointMake(0.0, contentOffset.y)]; } #pragma mark - UITextView Overrides - (void)setSelectedRange:(NSRange)selectedRange { [super setSelectedRange:selectedRange]; [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewSelectedRangeDidChangeNotification object:self userInfo:nil]; } - (void)setSelectedTextRange:(UITextRange *)selectedTextRange { [super setSelectedTextRange:selectedTextRange]; [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewSelectedRangeDidChangeNotification object:self userInfo:nil]; } - (void)setText:(NSString *)text { // Registers for undo management [self slk_prepareForUndo:@"Text Set"]; if (text) { [self setAttributedText:[self slk_defaultAttributedStringForText:text]]; } else { [self setAttributedText:nil]; } [[NSNotificationCenter defaultCenter] postNotificationName:UITextViewTextDidChangeNotification object:self]; } - (NSString *)text { return self.attributedText.string; } - (void)setAttributedText:(NSAttributedString *)attributedText { // Registers for undo management [self slk_prepareForUndo:@"Attributed Text Set"]; [super setAttributedText:attributedText]; [[NSNotificationCenter defaultCenter] postNotificationName:UITextViewTextDidChangeNotification object:self]; } - (void)setFont:(UIFont *)font { NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; [self setFontName:font.fontName pointSize:font.pointSize withContentSizeCategory:contentSizeCategory]; self.initialFontSize = font.pointSize; } - (void)setFontName:(NSString *)fontName pointSize:(CGFloat)pointSize withContentSizeCategory:(NSString *)contentSizeCategory { if (self.isDynamicTypeEnabled) { pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory); } UIFont *dynamicFont = [UIFont fontWithName:fontName size:pointSize]; [super setFont:dynamicFont]; // Updates the placeholder font too self.placeholderLabel.font = dynamicFont; } - (void)setDynamicTypeEnabled:(BOOL)dynamicTypeEnabled { if (self.isDynamicTypeEnabled == dynamicTypeEnabled) { return; } _dynamicTypeEnabled = dynamicTypeEnabled; NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; [self setFontName:self.font.fontName pointSize:self.initialFontSize withContentSizeCategory:contentSizeCategory]; } - (void)setTextAlignment:(NSTextAlignment)textAlignment { [super setTextAlignment:textAlignment]; // Updates the placeholder text alignment too self.placeholderLabel.textAlignment = textAlignment; } #pragma mark - UITextInput Overrides #ifdef __IPHONE_9_0 - (void)beginFloatingCursorAtPoint:(CGPoint)point { [super beginFloatingCursorAtPoint:point]; _trackpadEnabled = YES; } - (void)updateFloatingCursorAtPoint:(CGPoint)point { [super updateFloatingCursorAtPoint:point]; } - (void)endFloatingCursor { [super endFloatingCursor]; _trackpadEnabled = NO; // We still need to notify a selection change in the textview after the trackpad is disabled if (self.delegate && [self.delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) { [self.delegate textViewDidChangeSelection:self]; } [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewSelectedRangeDidChangeNotification object:self userInfo:nil]; } #endif #pragma mark - UIResponder Overrides - (BOOL)canBecomeFirstResponder { [self slk_addCustomMenuControllerItems]; return [super canBecomeFirstResponder]; } - (BOOL)becomeFirstResponder { return [super becomeFirstResponder]; } - (BOOL)canResignFirstResponder { // Removes undo/redo items if (self.undoManagerEnabled) { [self.undoManager removeAllActions]; } return [super canResignFirstResponder]; } - (BOOL)resignFirstResponder { return [super resignFirstResponder]; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { if (self.isFormatting) { NSString *title = [self slk_formattingTitleFromSelector:action]; NSString *symbol = [self slk_formattingSymbolWithTitle:title]; if (symbol.length > 0) { if (self.delegate && [self.delegate respondsToSelector:@selector(textView:shouldOfferFormattingForSymbol:)]) { return [self.delegate textView:self shouldOfferFormattingForSymbol:symbol]; } else { return YES; } } return NO; } if (action == @selector(delete:)) { return NO; } if (action == @selector(slk_presentFormattingMenu:)) { return self.selectedRange.length > 0 ? YES : NO; } if (action == @selector(paste:) && [self slk_isPasteboardItemSupported]) { return YES; } if (self.undoManagerEnabled) { if (action == @selector(slk_undo:)) { if (self.undoManager.undoActionIsDiscardable) { return NO; } return [self.undoManager canUndo]; } if (action == @selector(slk_redo:)) { if (self.undoManager.redoActionIsDiscardable) { return NO; } return [self.undoManager canRedo]; } } return [super canPerformAction:action withSender:sender]; } - (void)paste:(id)sender { id pastedItem = [self slk_pastedItem]; if ([pastedItem isKindOfClass:[NSDictionary class]]) { [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewDidPasteItemNotification object:nil userInfo:pastedItem]; } else if ([pastedItem isKindOfClass:[NSString class]]) { // Respect the delegate yo! if (self.delegate && [self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) { if (![self.delegate textView:self shouldChangeTextInRange:self.selectedRange replacementText:pastedItem]) { return; } } // Inserting the text fixes a UITextView bug whitch automatically scrolls to the bottom // and beyond scroll content size sometimes when the text is too long [self slk_insertTextAtCaretRange:pastedItem]; } } #pragma mark - NSObject Overrides - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { if ([super methodSignatureForSelector:sel]) { return [super methodSignatureForSelector:sel]; } return [super methodSignatureForSelector:@selector(slk_format:)]; } - (void)forwardInvocation:(NSInvocation *)invocation { NSString *title = [self slk_formattingTitleFromSelector:[invocation selector]]; if (title.length > 0) { [self slk_format:title]; } else { [super forwardInvocation:invocation]; } } #pragma mark - Custom Actions - (void)slk_flashScrollIndicatorsIfNeeded { if (self.numberOfLines == self.maxNumberOfLines+1) { if (!_didFlashScrollIndicators) { _didFlashScrollIndicators = YES; [super flashScrollIndicators]; } } else if (_didFlashScrollIndicators) { _didFlashScrollIndicators = NO; } } - (void)refreshFirstResponder { if (!self.isFirstResponder) { return; } _didNotResignFirstResponder = YES; [self resignFirstResponder]; _didNotResignFirstResponder = NO; [self becomeFirstResponder]; } - (void)refreshInputViews { _didNotResignFirstResponder = YES; [super reloadInputViews]; _didNotResignFirstResponder = NO; } - (void)slk_addCustomMenuControllerItems { UIMenuItem *undo = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"Undo", nil) action:@selector(slk_undo:)]; UIMenuItem *redo = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"Redo", nil) action:@selector(slk_redo:)]; NSMutableArray *items = [NSMutableArray arrayWithObjects:undo, redo, nil]; if (self.registeredFormattingTitles.count > 0) { UIMenuItem *format = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"Format", nil) action:@selector(slk_presentFormattingMenu:)]; [items addObject:format]; } [[UIMenuController sharedMenuController] setMenuItems:items]; } - (void)slk_undo:(id)sender { [self.undoManager undo]; } - (void)slk_redo:(id)sender { [self.undoManager redo]; } - (void)slk_presentFormattingMenu:(id)sender { NSMutableArray *items = [NSMutableArray arrayWithCapacity:self.registeredFormattingTitles.count]; for (NSString *name in self.registeredFormattingTitles) { NSString *sel = [NSString stringWithFormat:@"%@%@", SLKTextViewGenericFormattingSelectorPrefix, name]; UIMenuItem *item = [[UIMenuItem alloc] initWithTitle:name action:NSSelectorFromString(sel)]; [items addObject:item]; } self.formatting = YES; UIMenuController *menu = [UIMenuController sharedMenuController]; [menu setMenuItems:items]; NSLayoutManager *manager = self.layoutManager; CGRect targetRect = [manager boundingRectForGlyphRange:self.selectedRange inTextContainer:self.textContainer]; [menu setTargetRect:targetRect inView:self]; [menu setMenuVisible:YES animated:YES]; } - (NSString *)slk_formattingTitleFromSelector:(SEL)selector { NSString *selectorString = NSStringFromSelector(selector); NSRange match = [selectorString rangeOfString:SLKTextViewGenericFormattingSelectorPrefix]; if (match.location != NSNotFound) { return [selectorString substringFromIndex:SLKTextViewGenericFormattingSelectorPrefix.length]; } return nil; } - (NSString *)slk_formattingSymbolWithTitle:(NSString *)title { NSUInteger idx = [self.registeredFormattingTitles indexOfObject:title]; if (idx <= self.registeredFormattingSymbols.count -1) { return self.registeredFormattingSymbols[idx]; } return nil; } - (void)slk_format:(NSString *)titles { NSString *symbol = [self slk_formattingSymbolWithTitle:titles]; if (symbol.length > 0) { NSRange selection = self.selectedRange; NSRange range = [self slk_insertText:symbol inRange:NSMakeRange(selection.location, 0)]; range.location += selection.length; range.length = 0; // The default behavior is to add a closure BOOL addClosure = YES; if (self.delegate && [self.delegate respondsToSelector:@selector(textView:shouldInsertSuffixForFormattingWithSymbol:prefixRange:)]) { addClosure = [self.delegate textView:self shouldInsertSuffixForFormattingWithSymbol:symbol prefixRange:selection]; } if (addClosure) { self.selectedRange = [self slk_insertText:symbol inRange:range]; } } } #pragma mark - Markdown Formatting - (void)registerMarkdownFormattingSymbol:(NSString *)symbol withTitle:(NSString *)title { if (!symbol || !title) { return; } if (!_registeredFormattingTitles) { _registeredFormattingTitles = [NSMutableArray new]; _registeredFormattingSymbols = [NSMutableArray new]; } // Adds the symbol if not contained already if (![self.registeredSymbols containsObject:symbol]) { [self.registeredFormattingTitles addObject:title]; [self.registeredFormattingSymbols addObject:symbol]; } } - (NSArray *)registeredSymbols { return self.registeredFormattingSymbols; } #pragma mark - Notification Events - (void)slk_didBeginEditing:(NSNotification *)notification { if (![notification.object isEqual:self]) { return; } // Do something } - (void)slk_didChangeText:(NSNotification *)notification { if (![notification.object isEqual:self]) { return; } if (self.placeholderLabel.hidden != [self slk_shouldHidePlaceholder]) { [self setNeedsLayout]; } [self slk_flashScrollIndicatorsIfNeeded]; } - (void)slk_didEndEditing:(NSNotification *)notification { if (![notification.object isEqual:self]) { return; } // Do something } - (void)slk_didChangeTextInputMode:(NSNotification *)notification { // Do something } - (void)slk_didChangeContentSizeCategory:(NSNotification *)notification { if (!self.isDynamicTypeEnabled) { return; } NSString *contentSizeCategory = notification.userInfo[UIContentSizeCategoryNewValueKey]; [self setFontName:self.font.fontName pointSize:self.initialFontSize withContentSizeCategory:contentSizeCategory]; NSString *text = [self.text copy]; // Reloads the content size of the text view [self setText:@" "]; [self setText:text]; } - (void)slk_willShowMenuController:(NSNotification *)notification { // Do something } - (void)slk_didHideMenuController:(NSNotification *)notification { self.formatting = NO; [self slk_addCustomMenuControllerItems]; } #pragma mark - KVO Listener - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([object isEqual:self] && [keyPath isEqualToString:NSStringFromSelector(@selector(contentSize))]) { [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewContentSizeDidChangeNotification object:self userInfo:nil]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } #pragma mark - Motion Events - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { if (event.type == UIEventTypeMotion && event.subtype == UIEventSubtypeMotionShake) { [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewDidShakeNotification object:self]; } } #pragma mark - External Keyboard Support typedef void (^SLKKeyCommandHandler)(UIKeyCommand *keyCommand); - (void)observeKeyInput:(NSString *)input modifiers:(UIKeyModifierFlags)modifiers title:(NSString *_Nullable)title completion:(void (^)(UIKeyCommand *keyCommand))completion { NSAssert([input isKindOfClass:[NSString class]], @"You must provide a string with one or more characters corresponding to the keys to observe."); NSAssert(completion != nil, @"You must provide a non-nil completion block."); if (!input || !completion) { return; } UIKeyCommand *keyCommand = [UIKeyCommand keyCommandWithInput:input modifierFlags:modifiers action:@selector(didDetectKeyCommand:)]; #ifdef __IPHONE_9_0 if ([UIKeyCommand respondsToSelector:@selector(keyCommandWithInput:modifierFlags:action:discoverabilityTitle:)] ) { keyCommand.discoverabilityTitle = title; } #endif if (!_registeredKeyCommands) { _registeredKeyCommands = [NSMutableDictionary new]; _registeredKeyCallbacks = [NSMutableDictionary new]; } NSString *key = [self keyForKeyCommand:keyCommand]; self.registeredKeyCommands[key] = keyCommand; self.registeredKeyCallbacks[key] = completion; } - (void)didDetectKeyCommand:(UIKeyCommand *)keyCommand { NSString *key = [self keyForKeyCommand:keyCommand]; SLKKeyCommandHandler completion = self.registeredKeyCallbacks[key]; if (completion) { completion(keyCommand); } } - (NSString *)keyForKeyCommand:(UIKeyCommand *)keyCommand { return [NSString stringWithFormat:@"%@_%ld", keyCommand.input, (long)keyCommand.modifierFlags]; } - (NSArray *)keyCommands { if (self.registeredKeyCommands) { return [self.registeredKeyCommands allValues]; } return nil; } #pragma mark Up/Down Cursor Movement - (void)didPressArrowKey:(UIKeyCommand *)keyCommand { if (![keyCommand isKindOfClass:[UIKeyCommand class]] || self.text.length == 0 || self.numberOfLines < 2) { return; } if ([keyCommand.input isEqualToString:UIKeyInputUpArrow]) { [self slk_moveCursorTodirection:UITextLayoutDirectionUp]; } else if ([keyCommand.input isEqualToString:UIKeyInputDownArrow]) { [self slk_moveCursorTodirection:UITextLayoutDirectionDown]; } } - (void)slk_moveCursorTodirection:(UITextLayoutDirection)direction { UITextPosition *start = (direction == UITextLayoutDirectionUp) ? self.selectedTextRange.start : self.selectedTextRange.end; if ([self slk_isNewVerticalMovementForPosition:start inDirection:direction]) { self.verticalMoveDirection = direction; self.verticalMoveStartCaretRect = [self caretRectForPosition:start]; } if (start) { UITextPosition *end = [self slk_closestPositionToPosition:start inDirection:direction]; if (end) { self.verticalMoveLastCaretRect = [self caretRectForPosition:end]; self.selectedTextRange = [self textRangeFromPosition:end toPosition:end]; [self slk_scrollToCaretPositonAnimated:NO]; } } } // Based on code from Ruben Cabaco // https://gist.github.com/rcabaco/6765778 - (UITextPosition *)slk_closestPositionToPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction { // Only up/down are implemented. No real need for left/right since that is native to UITextInput. NSParameterAssert(direction == UITextLayoutDirectionUp || direction == UITextLayoutDirectionDown); // Translate the vertical direction to a horizontal direction. UITextLayoutDirection lookupDirection = (direction == UITextLayoutDirectionUp) ? UITextLayoutDirectionLeft : UITextLayoutDirectionRight; // Walk one character at a time in `lookupDirection` until the next line is reached. UITextPosition *checkPosition = position; UITextPosition *closestPosition = position; CGRect startingCaretRect = [self caretRectForPosition:position]; CGRect nextLineCaretRect = CGRectZero; BOOL isInNextLine = NO; while (YES) { UITextPosition *nextPosition = [self positionFromPosition:checkPosition inDirection:lookupDirection offset:1]; // End of line. if (!nextPosition || [self comparePosition:checkPosition toPosition:nextPosition] == NSOrderedSame) { break; } checkPosition = nextPosition; CGRect checkRect = [self caretRectForPosition:checkPosition]; if (CGRectGetMidY(startingCaretRect) != CGRectGetMidY(checkRect)) { // While on the next line stop just above/below the starting position. if (lookupDirection == UITextLayoutDirectionLeft && CGRectGetMidX(checkRect) <= CGRectGetMidX(self.verticalMoveStartCaretRect)) { closestPosition = checkPosition; break; } if (lookupDirection == UITextLayoutDirectionRight && CGRectGetMidX(checkRect) >= CGRectGetMidX(self.verticalMoveStartCaretRect)) { closestPosition = checkPosition; break; } // But don't skip lines. if (isInNextLine && CGRectGetMidY(checkRect) != CGRectGetMidY(nextLineCaretRect)) { break; } isInNextLine = YES; nextLineCaretRect = checkRect; closestPosition = checkPosition; } } return closestPosition; } - (BOOL)slk_isNewVerticalMovementForPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction { CGRect caretRect = [self caretRectForPosition:position]; BOOL noPreviousStartPosition = CGRectEqualToRect(self.verticalMoveStartCaretRect, CGRectZero); BOOL caretMovedSinceLastPosition = !CGRectEqualToRect(caretRect, self.verticalMoveLastCaretRect); BOOL directionChanged = self.verticalMoveDirection != direction; BOOL newMovement = noPreviousStartPosition || caretMovedSinceLastPosition || directionChanged; return newMovement; } #pragma mark - NSNotificationCenter registration - (void)slk_registerNotifications { [self slk_unregisterNotifications]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didBeginEditing:) name:UITextViewTextDidBeginEditingNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeText:) name:UITextViewTextDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didEndEditing:) name:UITextViewTextDidEndEditingNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeTextInputMode:) name:UITextInputCurrentInputModeDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeContentSizeCategory:) name:UIContentSizeCategoryDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_willShowMenuController:) name:UIMenuControllerWillShowMenuNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didHideMenuController:) name:UIMenuControllerDidHideMenuNotification object:nil]; } - (void)slk_unregisterNotifications { [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextInputCurrentInputModeDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIContentSizeCategoryDidChangeNotification object:nil]; } #pragma mark - Lifeterm - (void)dealloc { [self slk_unregisterNotifications]; [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(contentSize))]; } @end ================================================ FILE: Source/SLKTextViewController.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import #import #import "SLKTextInputbar.h" #import "SLKTextView.h" #import "SLKTypingIndicatorView.h" #import "SLKTypingIndicatorProtocol.h" #import "SLKTextView+SLKAdditions.h" #import "UIScrollView+SLKAdditions.h" #import "UIView+SLKAdditions.h" #import "SLKUIConstants.h" NS_ASSUME_NONNULL_BEGIN /** UIKeyboard notification replacement, posting reliably only when showing/hiding the keyboard (not when resizing keyboard, or with inputAccessoryView reloads, etc). Only triggered when using SLKTextViewController's text view. */ UIKIT_EXTERN NSString *const SLKKeyboardWillShowNotification; UIKIT_EXTERN NSString *const SLKKeyboardDidShowNotification; UIKIT_EXTERN NSString *const SLKKeyboardWillHideNotification; UIKIT_EXTERN NSString *const SLKKeyboardDidHideNotification; /** This feature doesn't work on iOS 9 due to no legit alternatives to detect the keyboard view. Open Radar: http://openradar.appspot.com/radar?id=5021485877952512 */ UIKIT_EXTERN NSString *const SLKTextInputbarDidMoveNotification; typedef NS_ENUM(NSUInteger, SLKKeyboardStatus) { SLKKeyboardStatusDidHide, SLKKeyboardStatusWillShow, SLKKeyboardStatusDidShow, SLKKeyboardStatusWillHide }; /** @name A drop-in UIViewController subclass with a growing text input view and other useful messaging features. */ NS_CLASS_AVAILABLE_IOS(7_0) @interface SLKTextViewController : UIViewController /** The main table view managed by the controller object. Created by default initializing with -init or initWithNibName:bundle: */ @property (nonatomic, readonly) UITableView *_Nullable tableView; /** The main collection view managed by the controller object. Not nil if the controller is initialised with -initWithCollectionViewLayout: */ @property (nonatomic, readonly) UICollectionView *_Nullable collectionView; /** The main scroll view managed by the controller object. Not nil if the controller is initialised with -initWithScrollView: */ @property (nonatomic, readonly) UIScrollView *_Nullable scrollView; /** The bottom toolbar containing a text view and buttons. */ @property (nonatomic, readonly) SLKTextInputbar *textInputbar; /** The default typing indicator used to display user names horizontally. */ @property (nonatomic, readonly) SLKTypingIndicatorView *_Nullable typingIndicatorView; /** The custom typing indicator view. Default is kind of SLKTypingIndicatorView. To customize the typing indicator view, you will need to call -registerClassForTypingIndicatorView: nside of any initialization method. To interact with it directly, you will need to cast the return value of -typingIndicatorProxyView to the appropriate type. */ @property (nonatomic, readonly) UIView *typingIndicatorProxyView; /** A single tap gesture used to dismiss the keyboard. SLKTextViewController is its delegate. */ @property (nonatomic, readonly) UIGestureRecognizer *singleTapGesture; /** A vertical pan gesture used for bringing the keyboard from the bottom. SLKTextViewController is its delegate. */ @property (nonatomic, readonly) UIPanGestureRecognizer *verticalPanGesture; /** YES if animations should have bouncy effects. Default is YES. */ @property (nonatomic, assign) BOOL bounces; /** YES if text view's content can be cleaned with a shake gesture. Default is NO. */ @property (nonatomic, assign) BOOL shakeToClearEnabled; /** YES if keyboard can be dismissed gradually with a vertical panning gesture. Default is YES. This feature doesn't work on iOS 9 due to no legit alternatives to detect the keyboard view. Open Radar: http://openradar.appspot.com/radar?id=5021485877952512 */ @property (nonatomic, assign, getter = isKeyboardPanningEnabled) BOOL keyboardPanningEnabled; /** YES if an external keyboard has been detected (this value updates only when the text view becomes first responder). */ @property (nonatomic, readonly, getter=isExternalKeyboardDetected) BOOL externalKeyboardDetected; /** YES if the keyboard has been detected as undocked or split (iPad Only). */ @property (nonatomic, readonly, getter=isKeyboardUndocked) BOOL keyboardUndocked; /** YES if after right button press, the text view is cleared out. Default is YES. */ @property (nonatomic, assign) BOOL shouldClearTextAtRightButtonPress; /** YES if the scrollView should scroll to bottom when the keyboard is shown. Default is NO.*/ @property (nonatomic, assign) BOOL shouldScrollToBottomAfterKeyboardShows; /** YES if the main table view is inverted. Default is YES. This allows the table view to start from the bottom like any typical messaging interface. If inverted, you must assign the same transform property to your cells to match the orientation (ie: cell.transform = tableView.transform;) Inverting the table view will enable some great features such as content offset corrections automatically when resizing the text input and/or showing autocompletion. */ @property (nonatomic, assign, getter = isInverted) BOOL inverted; /** YES if the view controller is presented inside of a popover controller. If YES, the keyboard won't move the text input bar and tapping on the tableView/collectionView will not cause the keyboard to be dismissed. This property is compatible only with iPad. */ @property (nonatomic, assign, getter = isPresentedInPopover) BOOL presentedInPopover; /** The current keyboard status (will/did hide, will/did show) */ @property (nonatomic, readonly) SLKKeyboardStatus keyboardStatus; /** Convenience accessors (accessed through the text input bar) */ @property (nonatomic, readonly) SLKTextView *textView; @property (nonatomic, readonly) UIButton *leftButton; @property (nonatomic, readonly) UIButton *rightButton; #pragma mark - Initialization ///------------------------------------------------ /// @name Initialization ///------------------------------------------------ /** Initializes a text view controller to manage a table view of a given style. If you use the standard -init method, a table view with plain style will be created. @param style A constant that specifies the style of main table view that the controller object is to manage (UITableViewStylePlain or UITableViewStyleGrouped). @return An initialized SLKTextViewController object or nil if the object could not be created. */ - (instancetype __nullable)initWithTableViewStyle:(UITableViewStyle)style; /** Initializes a collection view controller and configures the collection view with the provided layout. If you use the standard -init method, a table view with plain style will be created. @param layout The layout object to associate with the collection view. The layout controls how the collection view presents its cells and supplementary views. @return An initialized SLKTextViewController object or nil if the object could not be created. */ - (instancetype __nullable)initWithCollectionViewLayout:(UICollectionViewLayout *)layout; /** Initializes a text view controller to manage an arbitraty scroll view. The caller is responsible for configuration of the scroll view, including wiring the delegate. @param a UISCrollView to be used as the main content area. @return An initialized SLKTextViewController object or nil if the object could not be created. */ - (instancetype __nullable)initWithScrollView:(UIScrollView *)scrollView; /** Initializes either a table or collection view controller. You must override either +tableViewStyleForCoder: or +collectionViewLayoutForCoder: to define witch view to be layed out. @param decoder An unarchiver object. @return An initialized SLKTextViewController object or nil if the object could not be created. */ - (instancetype __nullable)initWithCoder:(NSCoder *)decoder; /** Returns the tableView style to be configured when using Interface Builder. Default is UITableViewStylePlain. You must override this method if you want to configure a tableView. @param decoder An unarchiver object. @return The tableView style to be used in the new instantiated tableView. */ + (UITableViewStyle)tableViewStyleForCoder:(NSCoder *)decoder; /** Returns the tableView style to be configured when using Interface Builder. Default is nil. You must override this method if you want to configure a collectionView. @param decoder An unarchiver object. @return The collectionView style to be used in the new instantiated collectionView. */ + (UICollectionViewLayout *)collectionViewLayoutForCoder:(NSCoder *)decoder; #pragma mark - Keyboard Handling ///------------------------------------------------ /// @name Keyboard Handling ///------------------------------------------------ /** Presents the keyboard, if not already, animated. You can override this method to perform additional tasks associated with presenting the keyboard. You SHOULD call super to inherit some conditionals. @param animated YES if the keyboard should show using an animation. */ - (void)presentKeyboard:(BOOL)animated; /** Dimisses the keyboard, if not already, animated. You can override this method to perform additional tasks associated with dismissing the keyboard. You SHOULD call super to inherit some conditionals. @param animated YES if the keyboard should be dismissed using an animation. */ - (void)dismissKeyboard:(BOOL)animated; /** Verifies if the text input bar should still move up/down even if it is NOT first responder. Default is NO. You can override this method to perform additional tasks associated with presenting the view. You don't need call super since this method doesn't do anything. @param responder The current first responder object. @return YES so the text input bar still move up/down. */ - (BOOL)forceTextInputbarAdjustmentForResponder:(UIResponder *_Nullable)responder; /** Verifies if the text input bar should still move up/down when the text view is first responder. This is very useful when presenting the view controller in a custom modal presentation, when there keyboard events are being handled externally to reframe the presented view. You SHOULD call super to inherit some conditionals. @return YES so the text input bar still move up/down. */ - (BOOL)ignoreTextInputbarAdjustment NS_REQUIRES_SUPER; /** Notifies the view controller that the keyboard changed status. You can override this method to perform additional tasks associated with presenting the view. You don't need call super since this method doesn't do anything. @param status The new keyboard status. */ - (void)didChangeKeyboardStatus:(SLKKeyboardStatus)status; #pragma mark - Interaction Notifications ///------------------------------------------------ /// @name Interaction Notifications ///------------------------------------------------ /** Notifies the view controller that the text will update. You can override this method to perform additional tasks associated with text changes. You MUST call super at some point in your implementation. */ - (void)textWillUpdate NS_REQUIRES_SUPER; /** Notifies the view controller that the text did update. You can override this method to perform additional tasks associated with text changes. You MUST call super at some point in your implementation. @param If YES, the text input bar will be resized using an animation. */ - (void)textDidUpdate:(BOOL)animated NS_REQUIRES_SUPER; /** Notifies the view controller that the text selection did change. Use this method a replacement of UITextViewDelegate's -textViewDidChangeSelection: which is not reliable enough when using third-party keyboards (they don't forward events properly sometimes). You can override this method to perform additional tasks associated with text changes. You MUST call super at some point in your implementation. */ - (void)textSelectionDidChange NS_REQUIRES_SUPER; /** Notifies the view controller when the left button's action has been triggered, manually. You can override this method to perform additional tasks associated with the left button. You don't need call super since this method doesn't do anything. @param sender The object calling this method. */ - (void)didPressLeftButton:(id _Nullable)sender; /** Notifies the view controller when the right button's action has been triggered, manually or by using the keyboard return key. You can override this method to perform additional tasks associated with the right button. You MUST call super at some point in your implementation. @param sender The object calling this method. */ - (void)didPressRightButton:(id _Nullable)sender NS_REQUIRES_SUPER; /** Verifies if the right button can be pressed. If NO, the button is disabled. You can override this method to perform additional tasks. You SHOULD call super to inherit some conditionals. @return YES if the right button can be pressed. */ - (BOOL)canPressRightButton; /** Notifies the view controller when the user has pasted a supported media content (images and/or videos). You can override this method to perform additional tasks associated with image/video pasting. You don't need to call super since this method doesn't do anything. Only supported pastable medias configured in SLKTextView will be forwarded (take a look at SLKPastableMediaType). @para userInfo The payload containing the media data, content and media types. */ - (void)didPasteMediaContent:(NSDictionary *)userInfo; /** Verifies that the typing indicator view should be shown. You can override this method to perform additional tasks. You SHOULD call super to inherit some conditionals. @return YES if the typing indicator view should be presented. */ - (BOOL)canShowTypingIndicator; /** Notifies the view controller when the user has shaked the device for undoing text typing. You can override this method to perform additional tasks associated with the shake gesture. Calling super will prompt a system alert view with undo option. This will not be called if 'undoShakingEnabled' is set to NO and/or if the text view's content is empty. */ - (void)willRequestUndo; /** Notifies the view controller when the user has pressed the Return key (↵) with an external keyboard. You can override this method to perform additional tasks. You MUST call super at some point in your implementation. @param keyCommand The UIKeyCommand object being recognized. */ - (void)didPressReturnKey:(UIKeyCommand * _Nullable)keyCommand NS_REQUIRES_SUPER; /** Notifies the view controller when the user has pressed the Escape key (Esc) with an external keyboard. You can override this method to perform additional tasks. You MUST call super at some point in your implementation. @param keyCommand The UIKeyCommand object being recognized. */ - (void)didPressEscapeKey:(UIKeyCommand * _Nullable)keyCommand NS_REQUIRES_SUPER; /** Notifies the view controller when the user has pressed the arrow key with an external keyboard. You can override this method to perform additional tasks. You MUST call super at some point in your implementation. @param keyCommand The UIKeyCommand object being recognized. */ - (void)didPressArrowKey:(UIKeyCommand * _Nullable)keyCommand NS_REQUIRES_SUPER; #pragma mark - Text Input Bar Adjustment ///------------------------------------------------ /// @name Text Input Bar Adjustment ///------------------------------------------------ /** YES if the text inputbar is hidden. Default is NO. */ @property (nonatomic, getter=isTextInputbarHidden) BOOL textInputbarHidden; /** Changes the visibility of the text input bar. Calling this method with the animated parameter set to NO is equivalent to setting the value of the toolbarHidden property directly. @param hidden Specify YES to hide the toolbar or NO to show it. @param animated Specify YES if you want the toolbar to be animated on or off the screen. */ - (void)setTextInputbarHidden:(BOOL)hidden animated:(BOOL)animated; #pragma mark - Text Edition ///------------------------------------------------ /// @name Text Edition ///------------------------------------------------ /** YES if the text editing mode is active. */ @property (nonatomic, readonly, getter = isEditing) BOOL editing; /** Re-uses the text layout for edition, displaying an accessory view on top of the text input bar with options (cancel & save). You can override this method to perform additional tasks You MUST call super at some point in your implementation. @param text The string text to edit. */ - (void)editText:(NSString *)text NS_REQUIRES_SUPER; /** Re-uses the text layout for edition, displaying an accessory view on top of the text input bar with options (cancel & save). You can override this method to perform additional tasks You MUST call super at some point in your implementation. @param attributedText The attributed text to edit. */ - (void)editAttributedText:(NSAttributedString *)attributedText NS_REQUIRES_SUPER; /** Notifies the view controller when the editing bar's right button's action has been triggered, manually or by using the external keyboard's Return key. You can override this method to perform additional tasks associated with accepting changes. You MUST call super at some point in your implementation. @param sender The object calling this method. */ - (void)didCommitTextEditing:(id)sender NS_REQUIRES_SUPER; /** Notifies the view controller when the editing bar's right button's action has been triggered, manually or by using the external keyboard's Esc key. You can override this method to perform additional tasks associated with accepting changes. You MUST call super at some point in your implementation. @param sender The object calling this method. */ - (void)didCancelTextEditing:(id)sender NS_REQUIRES_SUPER; #pragma mark - Text Auto-Completion ///------------------------------------------------ /// @name Text Auto-Completion ///------------------------------------------------ /** The table view used to display autocompletion results. */ @property (nonatomic, readonly) UITableView *autoCompletionView; /** YES if the autocompletion mode is active. */ @property (nonatomic, readonly, getter = isAutoCompleting) BOOL autoCompleting; /** The recently found prefix symbol used as prefix for autocompletion mode. */ @property (nonatomic, copy) NSString *_Nullable foundPrefix; /** The range of the found prefix in the text view content. */ @property (nonatomic) NSRange foundPrefixRange; /** The recently found word at the text view's caret position. */ @property (nonatomic, copy) NSString *_Nullable foundWord; /** An array containing all the registered prefix strings for autocompletion. */ @property (nonatomic, readonly, copy) NSSet *_Nullable registeredPrefixes; /** Registers any string prefix for autocompletion detection, like for user mentions or hashtags autocompletion. The prefix must be valid string (i.e: '@', '#', '\', and so on). Prefixes can be of any length. @param prefixes An array of prefix strings. */ - (void)registerPrefixesForAutoCompletion:(NSArray *_Nullable)prefixes; /** Verifies that controller is allowed to process the textView's text for auto-completion. You can override this method to disable momentarily the auto-completion feature, or to let it visible for longer time. You SHOULD call super to inherit some conditionals. @return YES if the controller is allowed to process the text for auto-completion. */ - (BOOL)shouldProcessTextForAutoCompletion; /** During text autocompletion, by default, auto-correction and spell checking are disabled. Doing so, refreshes the text input to get rid of the Quick Type bar. You can override this method to avoid disabling in some cases. @return YES if the controller should not hide the quick type bar. */ - (BOOL)shouldDisableTypingSuggestionForAutoCompletion; /** Notifies the view controller either the autocompletion prefix or word have changed. Use this method to modify your data source or fetch data asynchronously from an HTTP resource. Once your data source is ready, make sure to call -showAutoCompletionView: to display the view accordingly. You don't need call super since this method doesn't do anything. You SHOULD call super to inherit some conditionals. @param prefix The detected prefix. @param word The derected word. */ - (void)didChangeAutoCompletionPrefix:(NSString *)prefix andWord:(NSString *)word; /** Use this method to programatically show/hide the autocompletion view. Right before the view is shown, -reloadData is called. So avoid calling it manually. @param show YES if the autocompletion view should be shown. */ - (void)showAutoCompletionView:(BOOL)show; /** Use this method to programatically show the autocompletion view, with provided prefix and word to search. Right before the view is shown, -reloadData is called. So avoid calling it manually. @param prefix A prefix that is used to trigger autocompletion @param word A word to search for autocompletion @param prefixRange The range in which prefix spans. */ - (void)showAutoCompletionViewWithPrefix:(NSString *)prefix andWord:(NSString *)word prefixRange:(NSRange)prefixRange; /** Returns a custom height for the autocompletion view. Default is 0.0. You can override this method to return a custom height. @return The autocompletion view's height. */ - (CGFloat)heightForAutoCompletionView; /** Returns the maximum height for the autocompletion view. Default is 140 pts. You can override this method to return a custom max height. @return The autocompletion view's max height. */ - (CGFloat)maximumHeightForAutoCompletionView; /** Cancels and hides the autocompletion view, animated. */ - (void)cancelAutoCompletion; /** Accepts the autocompletion, replacing the detected word with a new string, keeping the prefix. This method is a convinience of -acceptAutoCompletionWithString:keepPrefix: @param string The string to be used for replacing autocompletion placeholders. */ - (void)acceptAutoCompletionWithString:(NSString *_Nullable)string; /** Accepts the autocompletion, replacing the detected word with a new string, and optionally replacing the prefix too. @param string The string to be used for replacing autocompletion placeholders. @param keepPrefix YES if the prefix shouldn't be overidden. */ - (void)acceptAutoCompletionWithString:(NSString *_Nullable)string keepPrefix:(BOOL)keepPrefix; #pragma mark - Text Caching ///------------------------------------------------ /// @name Text Caching ///------------------------------------------------ /** Returns the key to be associated with a given text to be cached. Default is nil. To enable text caching, you must override this method to return valid key. The text view will be populated automatically when the view controller is configured. You don't need to call super since this method doesn't do anything. @return The string key for which to enable text caching. */ - (nullable NSString *)keyForTextCaching; /** Removes the current view controller's cached text. To enable this, you must return a valid key string in -keyForTextCaching. */ - (void)clearCachedText; /** Removes all the cached text from disk. */ + (void)clearAllCachedText; /** Caches text to disk. */ - (void)cacheTextView; #pragma mark - Customization ///------------------------------------------------ /// @name Customization ///------------------------------------------------ /** Registers a class for customizing the behavior and appearance of the text view. You need to call this method inside of any initialization method. @param aClass A SLKTextView subclass. */ - (void)registerClassForTextView:(Class _Nullable)aClass; /** Registers a class for customizing the behavior and appearance of the typing indicator view. You need to call this method inside of any initialization method. Make sure to conform to SLKTypingIndicatorProtocol and implement the required methods. @param aClass A UIView subclass conforming to the SLKTypingIndicatorProtocol. */ - (void)registerClassForTypingIndicatorView:(Class _Nullable)aClass; #pragma mark - Delegate Methods Requiring Super ///------------------------------------------------ /// @name Delegate Methods Requiring Super ///------------------------------------------------ /** UITextViewDelegate */ - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text NS_REQUIRES_SUPER; /** SLKTextViewDelegate */ - (BOOL)textView:(SLKTextView *)textView shouldInsertSuffixForFormattingWithSymbol:(NSString *)symbol prefixRange:(NSRange)prefixRange NS_REQUIRES_SUPER; /** UIScrollViewDelegate */ - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView NS_REQUIRES_SUPER; - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate NS_REQUIRES_SUPER; - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView NS_REQUIRES_SUPER; - (void)scrollViewDidScroll:(UIScrollView *)scrollView NS_REQUIRES_SUPER; /** UIGestureRecognizerDelegate */ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer NS_REQUIRES_SUPER; /** UIAlertViewDelegate */ #ifndef __IPHONE_8_0 - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex NS_REQUIRES_SUPER; #endif #pragma mark - Life Cycle Methods Requiring Super ///------------------------------------------------ /// @name Life Cycle Methods Requiring Super ///------------------------------------------------ /** Configures view hierarchy and layout constraints. If you override these methods, make sure to call super. */ - (void)loadView NS_REQUIRES_SUPER; - (void)viewDidLoad NS_REQUIRES_SUPER; - (void)viewWillAppear:(BOOL)animated NS_REQUIRES_SUPER; - (void)viewDidAppear:(BOOL)animated NS_REQUIRES_SUPER; - (void)viewWillDisappear:(BOOL)animated NS_REQUIRES_SUPER; - (void)viewDidDisappear:(BOOL)animated NS_REQUIRES_SUPER; - (void)viewWillLayoutSubviews NS_REQUIRES_SUPER; - (void)viewDidLayoutSubviews NS_REQUIRES_SUPER; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/SLKTextViewController.m ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import "SLKTextViewController.h" #import "SLKInputAccessoryView.h" #import "UIResponder+SLKAdditions.h" #import "SLKUIConstants.h" /** Feature flagged while waiting to implement a more reliable technique. */ #define SLKBottomPanningEnabled 0 #define kSLKAlertViewClearTextTag [NSStringFromClass([SLKTextViewController class]) hash] NSString * const SLKKeyboardWillShowNotification = @"SLKKeyboardWillShowNotification"; NSString * const SLKKeyboardDidShowNotification = @"SLKKeyboardDidShowNotification"; NSString * const SLKKeyboardWillHideNotification = @"SLKKeyboardWillHideNotification"; NSString * const SLKKeyboardDidHideNotification = @"SLKKeyboardDidHideNotification"; CGFloat const SLKAutoCompletionViewDefaultHeight = 140.0; @interface SLKTextViewController () { CGPoint _scrollViewOffsetBeforeDragging; CGFloat _keyboardHeightBeforeDragging; } // The shared scrollView pointer, either a tableView or collectionView @property (nonatomic, weak) UIScrollView *scrollViewProxy; // A hairline displayed on top of the auto-completion view, to better separate the content from the control. @property (nonatomic, strong) UIView *autoCompletionHairline; // Auto-Layout height constraints used for updating their constants @property (nonatomic, strong) NSLayoutConstraint *scrollViewHC; @property (nonatomic, strong) NSLayoutConstraint *textInputbarHC; @property (nonatomic, strong) NSLayoutConstraint *typingIndicatorViewHC; @property (nonatomic, strong) NSLayoutConstraint *autoCompletionViewHC; @property (nonatomic, strong) NSLayoutConstraint *keyboardHC; // YES if the user is moving the keyboard with a gesture @property (nonatomic, assign, getter = isMovingKeyboard) BOOL movingKeyboard; // YES if the view controller did appear and everything is finished configurating. This allows blocking some layout animations among other things. @property (nonatomic, getter=isViewVisible) BOOL viewVisible; // YES if the view controller's view's size is changing by its parent (i.e. when its window rotates or is resized) @property (nonatomic, getter = isTransitioning) BOOL transitioning; // Optional classes to be used instead of the default ones. @property (nonatomic, strong) Class textViewClass; @property (nonatomic, strong) Class typingIndicatorViewClass; @end @implementation SLKTextViewController @synthesize tableView = _tableView; @synthesize collectionView = _collectionView; @synthesize scrollView = _scrollView; @synthesize typingIndicatorProxyView = _typingIndicatorProxyView; @synthesize textInputbar = _textInputbar; @synthesize autoCompletionView = _autoCompletionView; @synthesize autoCompleting = _autoCompleting; @synthesize scrollViewProxy = _scrollViewProxy; @synthesize presentedInPopover = _presentedInPopover; #pragma mark - Initializer - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { return [self initWithTableViewStyle:UITableViewStylePlain]; } - (instancetype)init { return [self initWithTableViewStyle:UITableViewStylePlain]; } - (instancetype)initWithTableViewStyle:(UITableViewStyle)style { NSAssert([self class] != [SLKTextViewController class], @"Oops! You must subclass SLKTextViewController."); NSAssert(style == UITableViewStylePlain || style == UITableViewStyleGrouped, @"Oops! You must pass a valid UITableViewStyle."); if (self = [super initWithNibName:nil bundle:nil]) { self.scrollViewProxy = [self tableViewWithStyle:style]; [self slk_commonInit]; } return self; } - (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout { NSAssert([self class] != [SLKTextViewController class], @"Oops! You must subclass SLKTextViewController."); NSAssert([layout isKindOfClass:[UICollectionViewLayout class]], @"Oops! You must pass a valid UICollectionViewLayout object."); if (self = [super initWithNibName:nil bundle:nil]) { self.scrollViewProxy = [self collectionViewWithLayout:layout]; [self slk_commonInit]; } return self; } - (instancetype)initWithScrollView:(UIScrollView *)scrollView { NSAssert([self class] != [SLKTextViewController class], @"Oops! You must subclass SLKTextViewController."); NSAssert([scrollView isKindOfClass:[UIScrollView class]], @"Oops! You must pass a valid UIScrollView object."); if (self = [super initWithNibName:nil bundle:nil]) { _scrollView = scrollView; _scrollView.translatesAutoresizingMaskIntoConstraints = NO; // Makes sure the scrollView plays nice with auto-layout self.scrollViewProxy = _scrollView; [self slk_commonInit]; } return self; } - (instancetype)initWithCoder:(NSCoder *)decoder { NSAssert([self class] != [SLKTextViewController class], @"Oops! You must subclass SLKTextViewController."); NSAssert([decoder isKindOfClass:[NSCoder class]], @"Oops! You must pass a valid decoder object."); if (self = [super initWithCoder:decoder]) { UITableViewStyle tableViewStyle = [[self class] tableViewStyleForCoder:decoder]; UICollectionViewLayout *collectionViewLayout = [[self class] collectionViewLayoutForCoder:decoder]; if ([collectionViewLayout isKindOfClass:[UICollectionViewLayout class]]) { self.scrollViewProxy = [self collectionViewWithLayout:collectionViewLayout]; } else { self.scrollViewProxy = [self tableViewWithStyle:tableViewStyle]; } [self slk_commonInit]; } return self; } - (void)slk_commonInit { [self slk_registerNotifications]; self.bounces = YES; self.inverted = YES; self.shakeToClearEnabled = NO; self.keyboardPanningEnabled = YES; self.shouldClearTextAtRightButtonPress = YES; self.shouldScrollToBottomAfterKeyboardShows = NO; self.automaticallyAdjustsScrollViewInsets = YES; self.extendedLayoutIncludesOpaqueBars = YES; } #pragma mark - View lifecycle - (void)loadView { [super loadView]; } - (void)viewDidLoad { [super viewDidLoad]; [self.view addSubview:self.scrollViewProxy]; [self.view addSubview:self.autoCompletionView]; [self.view addSubview:self.typingIndicatorProxyView]; [self.view addSubview:self.textInputbar]; [self slk_setupViewConstraints]; [self slk_registerKeyCommands]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // Invalidates this flag when the view appears self.textView.didNotResignFirstResponder = NO; // Forces laying out the recently added subviews and update their constraints [self.view layoutIfNeeded]; [UIView performWithoutAnimation:^{ // Reloads any cached text [self slk_reloadTextView]; }]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self.scrollViewProxy flashScrollIndicators]; self.viewVisible = YES; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // Stops the keyboard from being dismissed during the navigation controller's "swipe-to-pop" self.textView.didNotResignFirstResponder = self.isMovingFromParentViewController; self.viewVisible = NO; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; // Caches the text before it's too late! [self cacheTextView]; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; [self slk_adjustContentConfigurationIfNeeded]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; } - (void)viewSafeAreaInsetsDidChange { [super viewSafeAreaInsetsDidChange]; [self slk_updateViewConstraints]; } #pragma mark - Getters + (UITableViewStyle)tableViewStyleForCoder:(NSCoder *)decoder { return UITableViewStylePlain; } + (UICollectionViewLayout *)collectionViewLayoutForCoder:(NSCoder *)decoder { return nil; } - (UITableView *)tableViewWithStyle:(UITableViewStyle)style { if (!_tableView) { _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:style]; _tableView.translatesAutoresizingMaskIntoConstraints = NO; _tableView.scrollsToTop = YES; _tableView.dataSource = self; _tableView.delegate = self; _tableView.clipsToBounds = NO; [self slk_updateInsetAdjustmentBehavior]; } return _tableView; } - (UICollectionView *)collectionViewWithLayout:(UICollectionViewLayout *)layout { if (!_collectionView) { _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.backgroundColor = [UIColor whiteColor]; _collectionView.translatesAutoresizingMaskIntoConstraints = NO; _collectionView.scrollsToTop = YES; _collectionView.dataSource = self; _collectionView.delegate = self; } return _collectionView; } - (UITableView *)autoCompletionView { if (!_autoCompletionView) { _autoCompletionView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; _autoCompletionView.translatesAutoresizingMaskIntoConstraints = NO; _autoCompletionView.backgroundColor = [UIColor colorWithWhite:0.97 alpha:1.0]; _autoCompletionView.scrollsToTop = NO; _autoCompletionView.dataSource = self; _autoCompletionView.delegate = self; #ifdef __IPHONE_9_0 if ([_autoCompletionView respondsToSelector:@selector(cellLayoutMarginsFollowReadableWidth)]) { _autoCompletionView.cellLayoutMarginsFollowReadableWidth = NO; } #endif CGRect rect = CGRectZero; rect.size = CGSizeMake(CGRectGetWidth(self.view.frame), 0.5); _autoCompletionHairline = [[UIView alloc] initWithFrame:rect]; _autoCompletionHairline.autoresizingMask = UIViewAutoresizingFlexibleWidth; _autoCompletionHairline.backgroundColor = _autoCompletionView.separatorColor; [_autoCompletionView addSubview:_autoCompletionHairline]; } return _autoCompletionView; } - (SLKTextInputbar *)textInputbar { if (!_textInputbar) { _textInputbar = [[SLKTextInputbar alloc] initWithTextViewClass:self.textViewClass]; _textInputbar.translatesAutoresizingMaskIntoConstraints = NO; [_textInputbar.leftButton addTarget:self action:@selector(didPressLeftButton:) forControlEvents:UIControlEventTouchUpInside]; [_textInputbar.rightButton addTarget:self action:@selector(didPressRightButton:) forControlEvents:UIControlEventTouchUpInside]; [_textInputbar.editorLeftButton addTarget:self action:@selector(didCancelTextEditing:) forControlEvents:UIControlEventTouchUpInside]; [_textInputbar.editorRightButton addTarget:self action:@selector(didCommitTextEditing:) forControlEvents:UIControlEventTouchUpInside]; _textInputbar.textView.delegate = self; _verticalPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(slk_didPanTextInputBar:)]; _verticalPanGesture.delegate = self; [_textInputbar addGestureRecognizer:self.verticalPanGesture]; } return _textInputbar; } - (UIView *)typingIndicatorProxyView { if (!_typingIndicatorProxyView) { Class class = self.typingIndicatorViewClass ? : [SLKTypingIndicatorView class]; _typingIndicatorProxyView = [[class alloc] init]; _typingIndicatorProxyView.translatesAutoresizingMaskIntoConstraints = NO; _typingIndicatorProxyView.hidden = YES; [_typingIndicatorProxyView addObserver:self forKeyPath:@"visible" options:NSKeyValueObservingOptionNew context:nil]; } return _typingIndicatorProxyView; } - (SLKTypingIndicatorView *)typingIndicatorView { if ([_typingIndicatorProxyView isKindOfClass:[SLKTypingIndicatorView class]]) { return (SLKTypingIndicatorView *)self.typingIndicatorProxyView; } return nil; } - (BOOL)isPresentedInPopover { return _presentedInPopover && SLK_IS_IPAD; } - (BOOL)isTextInputbarHidden { return _textInputbar.hidden; } - (SLKTextView *)textView { return _textInputbar.textView; } - (UIButton *)leftButton { return _textInputbar.leftButton; } - (UIButton *)rightButton { return _textInputbar.rightButton; } - (UIModalPresentationStyle)modalPresentationStyle { if (self.navigationController) { return self.navigationController.modalPresentationStyle; } return [super modalPresentationStyle]; } - (CGFloat)slk_appropriateKeyboardHeightFromNotification:(NSNotification *)notification { // Let's first detect keyboard special states such as external keyboard, undocked or split layouts. [self slk_detectKeyboardStatesInNotification:notification]; if ([self ignoreTextInputbarAdjustment]) { return [self slk_appropriateBottomMargin]; } CGRect keyboardRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; return [self slk_appropriateKeyboardHeightFromRect:keyboardRect]; } - (CGFloat)slk_appropriateKeyboardHeightFromRect:(CGRect)rect { CGRect keyboardRect = [self.view convertRect:rect fromView:nil]; CGFloat viewHeight = CGRectGetHeight(self.view.bounds); CGFloat keyboardMinY = CGRectGetMinY(keyboardRect); CGFloat keyboardHeight = MAX(0.0, viewHeight - keyboardMinY); CGFloat bottomMargin = [self slk_appropriateBottomMargin]; // When the keyboard height is zero, we can assume there is no keyboard visible // In that case, let's see if there are any other views outside of the view hiearchy // requiring to adjust the text input bottom margin if (keyboardHeight < bottomMargin) { keyboardHeight = bottomMargin; } return keyboardHeight; } - (CGFloat)slk_appropriateBottomMargin { // A bottom margin is required if the view is extended out of it bounds if ((self.edgesForExtendedLayout & UIRectEdgeBottom) > 0) { UITabBar *tabBar = self.tabBarController.tabBar; // Considers the bottom tab bar, unless it will be hidden if (tabBar && !tabBar.hidden && !self.hidesBottomBarWhenPushed) { return CGRectGetHeight(tabBar.frame); } } // A bottom margin is required for iPhone X if (@available(iOS 11.0, *)) { if (!self.textInputbar.isHidden) { return self.view.safeAreaInsets.bottom; } } return 0.0; } - (CGFloat)slk_appropriateScrollViewHeight { CGFloat scrollViewHeight = CGRectGetHeight(self.view.bounds); scrollViewHeight -= self.keyboardHC.constant; scrollViewHeight -= self.textInputbarHC.constant; scrollViewHeight -= self.autoCompletionViewHC.constant; scrollViewHeight -= self.typingIndicatorViewHC.constant; if (scrollViewHeight < 0) return 0; else return scrollViewHeight; } - (CGFloat)slk_topBarsHeight { // No need to adjust if the edge isn't available if ((self.edgesForExtendedLayout & UIRectEdgeTop) == 0) { return 0.0; } CGFloat topBarsHeight = CGRectGetHeight(self.navigationController.navigationBar.frame); if ((SLK_IS_IPHONE && SLK_IS_LANDSCAPE && SLK_IS_IOS8_AND_HIGHER) || (SLK_IS_IPAD && self.modalPresentationStyle == UIModalPresentationFormSheet) || self.isPresentedInPopover) { return topBarsHeight; } topBarsHeight += CGRectGetHeight([UIApplication sharedApplication].statusBarFrame); return topBarsHeight; } - (NSString *)slk_appropriateKeyboardNotificationName:(NSNotification *)notification { NSString *name = notification.name; if ([name isEqualToString:UIKeyboardWillShowNotification]) { return SLKKeyboardWillShowNotification; } if ([name isEqualToString:UIKeyboardWillHideNotification]) { return SLKKeyboardWillHideNotification; } if ([name isEqualToString:UIKeyboardDidShowNotification]) { return SLKKeyboardDidShowNotification; } if ([name isEqualToString:UIKeyboardDidHideNotification]) { return SLKKeyboardDidHideNotification; } return nil; } - (SLKKeyboardStatus)slk_keyboardStatusForNotification:(NSNotification *)notification { NSString *name = notification.name; if ([name isEqualToString:UIKeyboardWillShowNotification]) { return SLKKeyboardStatusWillShow; } if ([name isEqualToString:UIKeyboardDidShowNotification]) { return SLKKeyboardStatusDidShow; } if ([name isEqualToString:UIKeyboardWillHideNotification]) { return SLKKeyboardStatusWillHide; } if ([name isEqualToString:UIKeyboardDidHideNotification]) { return SLKKeyboardStatusDidHide; } return -1; } - (BOOL)slk_isIllogicalKeyboardStatus:(SLKKeyboardStatus)newStatus { if ((self.keyboardStatus == SLKKeyboardStatusDidHide && newStatus == SLKKeyboardStatusWillShow) || (self.keyboardStatus == SLKKeyboardStatusWillShow && newStatus == SLKKeyboardStatusDidShow) || (self.keyboardStatus == SLKKeyboardStatusDidShow && newStatus == SLKKeyboardStatusWillHide) || (self.keyboardStatus == SLKKeyboardStatusWillHide && newStatus == SLKKeyboardStatusDidHide)) { return NO; } return YES; } #pragma mark - Setters - (void)setEdgesForExtendedLayout:(UIRectEdge)rectEdge { if (self.edgesForExtendedLayout == rectEdge) { return; } [super setEdgesForExtendedLayout:rectEdge]; [self slk_updateViewConstraints]; } - (void)setScrollViewProxy:(UIScrollView *)scrollView { if ([_scrollViewProxy isEqual:scrollView]) { return; } _singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(slk_didTapScrollView:)]; _singleTapGesture.delegate = self; [_singleTapGesture requireGestureRecognizerToFail:scrollView.panGestureRecognizer]; [scrollView addGestureRecognizer:self.singleTapGesture]; [scrollView.panGestureRecognizer addTarget:self action:@selector(slk_didPanTextInputBar:)]; _scrollViewProxy = scrollView; } - (void)setAutoCompleting:(BOOL)autoCompleting { if (_autoCompleting == autoCompleting) { return; } _autoCompleting = autoCompleting; self.scrollViewProxy.scrollEnabled = !autoCompleting; } - (void)setInverted:(BOOL)inverted { if (_inverted == inverted) { return; } _inverted = inverted; [self slk_updateInsetAdjustmentBehavior]; self.scrollViewProxy.transform = inverted ? CGAffineTransformMake(1, 0, 0, -1, 0, 0) : CGAffineTransformIdentity; } - (void)setBounces:(BOOL)bounces { _bounces = bounces; _textInputbar.bounces = bounces; } - (void)slk_updateInsetAdjustmentBehavior { // Deactivate automatic scrollView adjustment for inverted table view if (@available(iOS 11.0, *)) { if (self.isInverted) { _tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } else { _tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic; } } } - (BOOL)slk_updateKeyboardStatus:(SLKKeyboardStatus)status { // Skips if trying to update the same status if (_keyboardStatus == status) { return NO; } // Skips illogical conditions // Forces the keyboard status when didHide to avoid any inconsistency. if (status != SLKKeyboardStatusDidHide && [self slk_isIllogicalKeyboardStatus:status]) { return NO; } _keyboardStatus = status; [self didChangeKeyboardStatus:status]; return YES; } #pragma mark - Public & Subclassable Methods - (void)presentKeyboard:(BOOL)animated { // Skips if already first responder if ([self.textView isFirstResponder]) { return; } if (!animated) { [UIView performWithoutAnimation:^{ [self.textView becomeFirstResponder]; }]; } else { [self.textView becomeFirstResponder]; } } - (void)dismissKeyboard:(BOOL)animated { // Dismisses the keyboard from any first responder in the window. if (![self.textView isFirstResponder] && self.keyboardHC.constant > 0) { [self.view.window endEditing:NO]; } if (!animated) { [UIView performWithoutAnimation:^{ [self.textView resignFirstResponder]; }]; } else { [self.textView resignFirstResponder]; } } - (BOOL)forceTextInputbarAdjustmentForResponder:(UIResponder *)responder { return NO; } - (BOOL)ignoreTextInputbarAdjustment { if (self.isExternalKeyboardDetected || self.isKeyboardUndocked) { return YES; } return NO; } - (void)didChangeKeyboardStatus:(SLKKeyboardStatus)status { // No implementation here. Meant to be overriden in subclass. } - (void)textWillUpdate { // No implementation here. Meant to be overriden in subclass. } - (void)textDidUpdate:(BOOL)animated { if (self.isTextInputbarHidden) { return; } CGFloat inputbarHeight = _textInputbar.appropriateHeight; _textInputbar.rightButton.enabled = [self canPressRightButton]; _textInputbar.editorRightButton.enabled = [self canPressRightButton]; if (inputbarHeight != self.textInputbarHC.constant) { CGFloat inputBarHeightDelta = inputbarHeight - self.textInputbarHC.constant; CGPoint newOffset = CGPointMake(0, self.scrollViewProxy.contentOffset.y + inputBarHeightDelta); self.textInputbarHC.constant = inputbarHeight; self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight]; if (animated) { BOOL bounces = self.bounces && [self.textView isFirstResponder]; __weak typeof(self) weakSelf = self; [self.view slk_animateLayoutIfNeededWithBounce:bounces options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionLayoutSubviews|UIViewAnimationOptionBeginFromCurrentState animations:^{ if (!self.isInverted) { self.scrollViewProxy.contentOffset = newOffset; } if (weakSelf.textInputbar.isEditing) { [weakSelf.textView slk_scrollToCaretPositonAnimated:NO]; } }]; } else { [self.view layoutIfNeeded]; } } // Toggles auto-correction if requiered [self slk_enableTypingSuggestionIfNeeded]; } - (void)textSelectionDidChange { // The text view must be first responder if (![self.textView isFirstResponder] || self.keyboardStatus != SLKKeyboardStatusDidShow) { return; } // Skips there is a real text selection if (self.textView.isTrackpadEnabled) { return; } if (self.textView.selectedRange.length > 0) { if (self.isAutoCompleting && [self shouldProcessTextForAutoCompletion]) { [self cancelAutoCompletion]; } return; } // Process the text at every caret movement [self slk_processTextForAutoCompletion]; } - (BOOL)canPressRightButton { NSString *text = [self.textView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (text.length > 0 && ![_textInputbar limitExceeded]) { return YES; } return NO; } - (void)didPressLeftButton:(id)sender { // No implementation here. Meant to be overriden in subclass. } - (void)didPressRightButton:(id)sender { if (self.shouldClearTextAtRightButtonPress) { // Clears the text and the undo manager [self.textView slk_clearText:YES]; } // Clears cache [self clearCachedText]; } - (void)editText:(NSString *)text { NSAttributedString *attributedText = [self.textView slk_defaultAttributedStringForText:text]; [self editAttributedText:attributedText]; } - (void)editAttributedText:(NSAttributedString *)attributedText { if (![_textInputbar canEditText:attributedText.string]) { return; } // Caches the current text, in case the user cancels the edition [self slk_cacheAttributedTextToDisk:self.textView.attributedText]; [_textInputbar beginTextEditing]; // Setting the text after calling -beginTextEditing is safer [self.textView setAttributedText:attributedText]; [self.textView slk_scrollToCaretPositonAnimated:YES]; // Brings up the keyboard if needed [self presentKeyboard:YES]; } - (void)didCommitTextEditing:(id)sender { if (!_textInputbar.isEditing) { return; } [_textInputbar endTextEdition]; // Clears the text and but not the undo manager [self.textView slk_clearText:NO]; } - (void)didCancelTextEditing:(id)sender { if (!_textInputbar.isEditing) { return; } [_textInputbar endTextEdition]; // Clears the text and but not the undo manager [self.textView slk_clearText:NO]; // Restores any previous cached text before entering in editing mode [self slk_reloadTextView]; } - (BOOL)canShowTypingIndicator { // Don't show if the text is being edited or auto-completed. if (_textInputbar.isEditing || self.isAutoCompleting) { return NO; } return YES; } - (CGFloat)heightForAutoCompletionView { return 0.0; } - (CGFloat)maximumHeightForAutoCompletionView { CGFloat maxiumumHeight = SLKAutoCompletionViewDefaultHeight; if (self.isAutoCompleting) { CGFloat scrollViewHeight = self.scrollViewHC.constant; scrollViewHeight -= [self slk_topBarsHeight]; if (scrollViewHeight < maxiumumHeight) { maxiumumHeight = scrollViewHeight; } } return maxiumumHeight; } - (void)didPasteMediaContent:(NSDictionary *)userInfo { // No implementation here. Meant to be overriden in subclass. } - (void)willRequestUndo { NSString *title = NSLocalizedString(@"Undo Typing", nil); NSString *acceptTitle = NSLocalizedString(@"Undo", nil); NSString *cancelTitle = NSLocalizedString(@"Cancel", nil); #ifdef __IPHONE_8_0 UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:acceptTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // Clears the text but doesn't clear the undo manager if (self.shakeToClearEnabled) { [self.textView slk_clearText:NO]; } }]]; [alertController addAction:[UIAlertAction actionWithTitle:cancelTitle style:UIAlertActionStyleCancel handler:NULL]]; [self presentViewController:alertController animated:YES completion:nil]; #else UIAlertView *alert = [UIAlertView new]; [alert setTitle:title]; [alert addButtonWithTitle:acceptTitle]; [alert addButtonWithTitle:cancelTitle]; [alert setCancelButtonIndex:1]; [alert setTag:kSLKAlertViewClearTextTag]; [alert setDelegate:self]; [alert show]; #endif } - (void)setTextInputbarHidden:(BOOL)hidden { [self setTextInputbarHidden:hidden animated:NO]; } - (void)setTextInputbarHidden:(BOOL)hidden animated:(BOOL)animated { if (self.isTextInputbarHidden == hidden) { return; } _textInputbar.hidden = hidden; if (@available(iOS 11.0, *)) { [self viewSafeAreaInsetsDidChange]; } __weak typeof(self) weakSelf = self; void (^animations)(void) = ^void(){ weakSelf.textInputbarHC.constant = hidden ? 0.0 : weakSelf.textInputbar.appropriateHeight; [weakSelf.view layoutIfNeeded]; }; void (^completion)(BOOL finished) = ^void(BOOL finished){ if (hidden) { [self dismissKeyboard:YES]; } }; if (animated) { [UIView animateWithDuration:0.25 animations:animations completion:completion]; } else { animations(); completion(NO); } } #pragma mark - Private Methods - (void)slk_didPanTextInputBar:(UIPanGestureRecognizer *)gesture { // Textinput dragging isn't supported when if (!self.view.window || !self.keyboardPanningEnabled || [self ignoreTextInputbarAdjustment] || self.isPresentedInPopover) { return; } dispatch_async(dispatch_get_main_queue(), ^{ [self slk_handlePanGestureRecognizer:gesture]; }); } - (void)slk_handlePanGestureRecognizer:(UIPanGestureRecognizer *)gesture { // Local variables static CGPoint startPoint; static CGRect originalFrame; static BOOL dragging = NO; static BOOL presenting = NO; __block UIView *keyboardView = [_textInputbar.inputAccessoryView keyboardViewProxy]; // When no keyboard view has been detecting, let's skip any handling. if (!keyboardView) { return; } // Dynamic variables CGPoint gestureLocation = [gesture locationInView:self.view]; CGPoint gestureVelocity = [gesture velocityInView:self.view]; CGFloat keyboardMaxY = CGRectGetHeight(SLKKeyWindowBounds()); CGFloat keyboardMinY = keyboardMaxY - CGRectGetHeight(keyboardView.frame); // Skips this if it's not the expected textView. // Checking the keyboard height constant helps to disable the view constraints update on iPad when the keyboard is undocked. // Checking the keyboard status allows to keep the inputAccessoryView valid when still reacing the bottom of the screen. CGFloat bottomMargin = [self slk_appropriateBottomMargin]; if (![self.textView isFirstResponder] || (self.keyboardHC.constant == bottomMargin && self.keyboardStatus == SLKKeyboardStatusDidHide)) { #if SLKBottomPanningEnabled if ([gesture.view isEqual:self.scrollViewProxy]) { if (gestureVelocity.y > 0) { return; } else if ((self.isInverted && ![self.scrollViewProxy slk_isAtTop]) || (!self.isInverted && ![self.scrollViewProxy slk_isAtBottom])) { return; } } presenting = YES; #else if ([gesture.view isEqual:_textInputbar] && gestureVelocity.y < 0) { [self presentKeyboard:YES]; } return; #endif } switch (gesture.state) { case UIGestureRecognizerStateBegan: { startPoint = CGPointZero; dragging = NO; if (presenting) { // Let's first present the keyboard without animation [self presentKeyboard:NO]; // So we can capture the keyboard's view keyboardView = [_textInputbar.inputAccessoryView keyboardViewProxy]; originalFrame = keyboardView.frame; originalFrame.origin.y = CGRectGetMaxY(self.view.frame); // And move the keyboard to the bottom edge // TODO: Fix an occasional layout glitch when the keyboard appears for the first time. keyboardView.frame = originalFrame; } break; } case UIGestureRecognizerStateChanged: { if (CGRectContainsPoint(_textInputbar.frame, gestureLocation) || dragging || presenting){ if (CGPointEqualToPoint(startPoint, CGPointZero)) { startPoint = gestureLocation; dragging = YES; if (!presenting) { originalFrame = keyboardView.frame; } } self.movingKeyboard = YES; CGPoint transition = CGPointMake(gestureLocation.x - startPoint.x, gestureLocation.y - startPoint.y); CGRect keyboardFrame = originalFrame; if (presenting) { keyboardFrame.origin.y += transition.y; } else { keyboardFrame.origin.y += MAX(transition.y, 0.0); } // Makes sure they keyboard is always anchored to the bottom if (CGRectGetMinY(keyboardFrame) < keyboardMinY) { keyboardFrame.origin.y = keyboardMinY; } keyboardView.frame = keyboardFrame; self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromRect:keyboardFrame]; self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight]; // layoutIfNeeded must be called before any further scrollView internal adjustments (content offset and size) [self.view layoutIfNeeded]; // Overrides the scrollView's contentOffset to allow following the same position when dragging the keyboard CGPoint offset = _scrollViewOffsetBeforeDragging; if (self.isInverted) { if (!self.scrollViewProxy.isDecelerating && self.scrollViewProxy.isTracking) { self.scrollViewProxy.contentOffset = _scrollViewOffsetBeforeDragging; } } else { CGFloat keyboardHeightDelta = _keyboardHeightBeforeDragging-self.keyboardHC.constant; offset.y -= keyboardHeightDelta; self.scrollViewProxy.contentOffset = offset; } } break; } case UIGestureRecognizerStatePossible: case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateFailed: { if (!dragging) { break; } CGPoint transition = CGPointMake(0.0, fabs(gestureLocation.y - startPoint.y)); CGRect keyboardFrame = originalFrame; if (presenting) { keyboardFrame.origin.y = keyboardMinY; } // The velocity can be changed to hide or show the keyboard based on the gesture CGFloat minVelocity = 20.0; CGFloat minDistance = CGRectGetHeight(keyboardFrame)/2.0; BOOL hide = (gestureVelocity.y > minVelocity) || (presenting && transition.y < minDistance) || (!presenting && transition.y > minDistance); if (hide) keyboardFrame.origin.y = keyboardMaxY; self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromRect:keyboardFrame]; self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight]; [UIView animateWithDuration:0.25 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionBeginFromCurrentState animations:^{ [self.view layoutIfNeeded]; keyboardView.frame = keyboardFrame; } completion:^(BOOL finished) { if (hide) { [self dismissKeyboard:NO]; } // Tear down startPoint = CGPointZero; originalFrame = CGRectZero; dragging = NO; presenting = NO; self.movingKeyboard = NO; }]; break; } default: break; } } - (void)slk_didTapScrollView:(UIGestureRecognizer *)gesture { if (!self.isPresentedInPopover && ![self ignoreTextInputbarAdjustment]) { [self dismissKeyboard:YES]; } } - (void)slk_didPanTextView:(UIGestureRecognizer *)gesture { [self presentKeyboard:YES]; } - (void)slk_performRightAction { NSArray *actions = [self.rightButton actionsForTarget:self forControlEvent:UIControlEventTouchUpInside]; if (actions.count > 0 && [self canPressRightButton]) { [self.rightButton sendActionsForControlEvents:UIControlEventTouchUpInside]; } } - (void)slk_postKeyboarStatusNotification:(NSNotification *)notification { if ([self ignoreTextInputbarAdjustment] || self.isTransitioning) { return; } NSMutableDictionary *userInfo = [notification.userInfo mutableCopy]; CGRect beginFrame = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue]; CGRect endFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; // Fixes iOS7 oddness with inverted values on landscape orientation if (!SLK_IS_IOS8_AND_HIGHER && SLK_IS_LANDSCAPE) { beginFrame = SLKRectInvert(beginFrame); endFrame = SLKRectInvert(endFrame); } CGFloat keyboardHeight = CGRectGetHeight(endFrame); beginFrame.size.height = keyboardHeight; endFrame.size.height = keyboardHeight; [userInfo setObject:[NSValue valueWithCGRect:beginFrame] forKey:UIKeyboardFrameBeginUserInfoKey]; [userInfo setObject:[NSValue valueWithCGRect:endFrame] forKey:UIKeyboardFrameEndUserInfoKey]; NSString *name = [self slk_appropriateKeyboardNotificationName:notification]; [[NSNotificationCenter defaultCenter] postNotificationName:name object:self.textView userInfo:userInfo]; } - (void)slk_enableTypingSuggestionIfNeeded { if (![self.textView isFirstResponder]) { return; } BOOL enable = !self.isAutoCompleting; NSString *inputPrimaryLanguage = self.textView.textInputMode.primaryLanguage; // Toggling autocorrect on Japanese keyboards breaks autocompletion by replacing the autocompletion prefix by an empty string. // So for now, let's not disable autocorrection for Japanese. if ([inputPrimaryLanguage isEqualToString:@"ja-JP"]) { return; } // Let's avoid refreshing the text view while dictation mode is enabled. // This solves a crash some users were experiencing when auto-completing with the dictation input mode. if ([inputPrimaryLanguage isEqualToString:@"dictation"]) { return; } if (enable == NO && ![self shouldDisableTypingSuggestionForAutoCompletion]) { return; } [self.textView setTypingSuggestionEnabled:enable]; } - (void)slk_dismissTextInputbarIfNeeded { CGFloat bottomMargin = [self slk_appropriateBottomMargin]; if (self.keyboardHC.constant == bottomMargin) { return; } self.keyboardHC.constant = bottomMargin; self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight]; [self slk_hideAutoCompletionViewIfNeeded]; [self.view layoutIfNeeded]; } - (void)slk_detectKeyboardStatesInNotification:(NSNotification *)notification { // Tear down _externalKeyboardDetected = NO; _keyboardUndocked = NO; if (self.isMovingKeyboard) { return; } // Based on http://stackoverflow.com/a/5760910/287403 // We can determine if the external keyboard is showing by adding the origin.y of the target finish rect (end when showing, begin when hiding) to the inputAccessoryHeight. // If it's greater(or equal) the window height, it's an external keyboard. CGRect beginRect = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue]; CGRect endRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; // Grab the base view for conversions as we don't want window coordinates in < iOS 8 // iOS 8 fixes the whole coordinate system issue for us, but iOS 7 doesn't rotate the app window coordinate space. UIView *baseView = self.view.window.rootViewController.view; CGRect screenBounds = [UIScreen mainScreen].bounds; // Convert the main screen bounds into the correct coordinate space but ignore the origin. CGRect viewBounds = [self.view convertRect:SLKKeyWindowBounds() fromView:nil]; viewBounds = CGRectMake(0, 0, viewBounds.size.width, viewBounds.size.height); // We want these rects in the correct coordinate space as well. CGRect convertBegin = [baseView convertRect:beginRect fromView:nil]; CGRect convertEnd = [baseView convertRect:endRect fromView:nil]; if ([notification.name isEqualToString:UIKeyboardWillShowNotification]) { if (convertEnd.origin.y >= viewBounds.size.height) { _externalKeyboardDetected = YES; } } else if ([notification.name isEqualToString:UIKeyboardWillHideNotification]) { // The additional logic check here (== to width) accounts for a glitch (iOS 8 only?) where the window has rotated it's coordinates // but the beginRect doesn't yet reflect that. It should never cause a false positive. if (convertBegin.origin.y >= viewBounds.size.height || convertBegin.origin.y == viewBounds.size.width) { _externalKeyboardDetected = YES; } } if (SLK_IS_IPAD && CGRectGetMaxY(convertEnd) < CGRectGetMaxY(screenBounds)) { // The keyboard is undocked or split (iPad Only) _keyboardUndocked = YES; // An external keyboard cannot be detected anymore _externalKeyboardDetected = NO; } } - (void)slk_adjustContentConfigurationIfNeeded { UIEdgeInsets contentInset = self.scrollViewProxy.contentInset; // When inverted, we need to substract the top bars height (generally status bar + navigation bar's) to align the top of the // scrollView correctly to its top edge. if (self.inverted) { contentInset.bottom = [self slk_topBarsHeight]; contentInset.top = contentInset.bottom > 0.0 ? 0.0 : contentInset.top; } else { contentInset.bottom = 0.0; } self.scrollViewProxy.contentInset = contentInset; self.scrollViewProxy.scrollIndicatorInsets = contentInset; } - (void)slk_prepareForInterfaceTransitionWithDuration:(NSTimeInterval)duration { self.transitioning = YES; [self.view layoutIfNeeded]; if ([self.textView isFirstResponder]) { [self.textView slk_scrollToCaretPositonAnimated:NO]; } else { [self.textView slk_scrollToBottomAnimated:NO]; } // Disables the flag after the rotation animation is finished // Hacky but works. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ self.transitioning = NO; }); } #pragma mark - Keyboard Events - (void)didPressReturnKey:(UIKeyCommand *)keyCommand { if (_textInputbar.isEditing) { [self didCommitTextEditing:keyCommand]; } else { [self slk_performRightAction]; } } - (void)didPressEscapeKey:(UIKeyCommand *)keyCommand { if (self.isAutoCompleting) { [self cancelAutoCompletion]; } else if (_textInputbar.isEditing) { [self didCancelTextEditing:keyCommand]; } CGFloat bottomMargin = [self slk_appropriateBottomMargin]; if ([self ignoreTextInputbarAdjustment] || ([self.textView isFirstResponder] && self.keyboardHC.constant == bottomMargin)) { return; } [self dismissKeyboard:YES]; } - (void)didPressArrowKey:(UIKeyCommand *)keyCommand { [self.textView didPressArrowKey:keyCommand]; } #pragma mark - Notification Events - (void)slk_willShowOrHideKeyboard:(NSNotification *)notification { SLKKeyboardStatus status = [self slk_keyboardStatusForNotification:notification]; // Skips if the view isn't visible. if (!self.isViewVisible) { return; } // Skips if it is presented inside of a popover. if (self.isPresentedInPopover) { return; } // Skips if textview did refresh only. if (self.textView.didNotResignFirstResponder) { return; } UIResponder *currentResponder = [UIResponder slk_currentFirstResponder]; // Skips if it's not the expected textView and shouldn't force adjustment of the text input bar. // This will also dismiss the text input bar if it's visible, and exit auto-completion mode if enabled. if (currentResponder && ![currentResponder isEqual:self.textView] && ![self forceTextInputbarAdjustmentForResponder:currentResponder]) { [self slk_dismissTextInputbarIfNeeded]; return; } // Skips if it's the current status if (self.keyboardStatus == status) { return; } // Programatically stops scrolling before updating the view constraints (to avoid scrolling glitch). if (status == SLKKeyboardStatusWillShow) { [self.scrollViewProxy slk_stopScrolling]; } // Stores the previous keyboard height CGFloat previousKeyboardHeight = self.keyboardHC.constant; // Updates the height constraints' constants self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromNotification:notification]; self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight]; // Updates and notifies about the keyboard status update if ([self slk_updateKeyboardStatus:status]) { // Posts custom keyboard notification, if logical conditions apply [self slk_postKeyboarStatusNotification:notification]; } // Hides the auto-completion view if the keyboard is being dismissed. if (![self.textView isFirstResponder] || status == SLKKeyboardStatusWillHide) { [self slk_hideAutoCompletionViewIfNeeded]; } UIScrollView *scrollView = self.scrollViewProxy; NSInteger curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; NSTimeInterval duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; CGRect beginFrame = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue]; CGRect endFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; void (^animations)(void) = ^void() { // Scrolls to bottom only if the keyboard is about to show. if (self.shouldScrollToBottomAfterKeyboardShows && self.keyboardStatus == SLKKeyboardStatusWillShow) { if (self.isInverted) { [scrollView slk_scrollToTopAnimated:YES]; } else { [scrollView slk_scrollToBottomAnimated:YES]; } } }; // Begin and end frames are the same when the keyboard is shown during navigation controller's push animation. // The animation happens in window coordinates (slides from right to left) but doesn't in the view controller's view coordinates. // Second condition: check if the height of the keyboard changed. if (!CGRectEqualToRect(beginFrame, endFrame) || fabs(previousKeyboardHeight - self.keyboardHC.constant) > 0.0) { // Content Offset correction if not inverted and not auto-completing. if (!self.isInverted && !self.isAutoCompleting) { CGFloat scrollViewHeight = self.scrollViewHC.constant; CGFloat keyboardHeight = self.keyboardHC.constant; CGSize contentSize = scrollView.contentSize; CGPoint contentOffset = scrollView.contentOffset; CGFloat newOffset = MIN(contentSize.height - scrollViewHeight, contentOffset.y + keyboardHeight - previousKeyboardHeight); scrollView.contentOffset = CGPointMake(contentOffset.x, newOffset); } // Only for this animation, we set bo to bounce since we want to give the impression that the text input is glued to the keyboard. [self.view slk_animateLayoutIfNeededWithDuration:duration bounce:NO options:(curve<<16)|UIViewAnimationOptionLayoutSubviews|UIViewAnimationOptionBeginFromCurrentState animations:animations completion:NULL]; } else { animations(); } } - (void)slk_didShowOrHideKeyboard:(NSNotification *)notification { SLKKeyboardStatus status = [self slk_keyboardStatusForNotification:notification]; // Skips if the view isn't visible if (!self.isViewVisible) { if (status == SLKKeyboardStatusDidHide && self.keyboardStatus == SLKKeyboardStatusWillHide) { // Even if the view isn't visible anymore, let's still continue to update all states. } else { return; } } // Skips if it is presented inside of a popover if (self.isPresentedInPopover) { return; } // Skips if textview did refresh only if (self.textView.didNotResignFirstResponder) { return; } // Skips if it's the current status if (self.keyboardStatus == status) { return; } // Updates and notifies about the keyboard status update if ([self slk_updateKeyboardStatus:status]) { // Posts custom keyboard notification, if logical conditions apply [self slk_postKeyboarStatusNotification:notification]; } // After showing keyboard, check if the current cursor position could diplay autocompletion if ([self.textView isFirstResponder] && status == SLKKeyboardStatusDidShow && !self.isAutoCompleting) { // Wait till the end of the current run loop dispatch_async(dispatch_get_main_queue(), ^{ [self slk_processTextForAutoCompletion]; }); } // Very important to invalidate this flag after the keyboard is dismissed or presented, to start with a clean state next time. self.movingKeyboard = NO; } - (void)slk_didPostSLKKeyboardNotification:(NSNotification *)notification { if (![notification.object isEqual:self.textView]) { return; } // Used for debug only NSLog(@"%@ %s: %@", NSStringFromClass([self class]), __FUNCTION__, notification); } - (void)slk_willChangeTextViewText:(NSNotification *)notification { // Skips this it's not the expected textView. if (![notification.object isEqual:self.textView]) { return; } [self textWillUpdate]; } - (void)slk_didChangeTextViewText:(NSNotification *)notification { // Skips this it's not the expected textView. if (![notification.object isEqual:self.textView]) { return; } // Animated only if the view already appeared. [self textDidUpdate:self.isViewVisible]; // Process the text at every change, when the view is visible if (self.isViewVisible) { [self slk_processTextForAutoCompletion]; } } - (void)slk_didChangeTextViewContentSize:(NSNotification *)notification { // Skips this it's not the expected textView. if (![notification.object isEqual:self.textView]) { return; } // Animated only if the view already appeared. [self textDidUpdate:self.isViewVisible]; } - (void)slk_didChangeTextViewSelectedRange:(NSNotification *)notification { // Skips this it's not the expected textView. if (![notification.object isEqual:self.textView]) { return; } [self textSelectionDidChange]; } - (void)slk_didChangeTextViewPasteboard:(NSNotification *)notification { // Skips this if it's not the expected textView. if (![self.textView isFirstResponder]) { return; } // Notifies only if the pasted item is nested in a dictionary. if (notification.userInfo) { [self didPasteMediaContent:notification.userInfo]; } } - (void)slk_didShakeTextView:(NSNotification *)notification { // Skips this if it's not the expected textView. if (![self.textView isFirstResponder]) { return; } // Notifies of the shake gesture if undo mode is on and the text view is not empty if (self.shakeToClearEnabled && self.textView.text.length > 0) { [self willRequestUndo]; } } - (void)slk_willShowOrHideTypeIndicatorView:(UIView *)view { // Skips if the typing indicator should not show. Ignores the checking if it's trying to hide. if (![self canShowTypingIndicator] && view.isVisible) { return; } CGFloat systemLayoutSizeHeight = [view systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; CGFloat height = view.isVisible ? systemLayoutSizeHeight : 0.0; self.typingIndicatorViewHC.constant = height; self.scrollViewHC.constant -= height; if (view.isVisible) { view.hidden = NO; } [self.view slk_animateLayoutIfNeededWithBounce:self.bounces options:UIViewAnimationOptionCurveEaseInOut animations:NULL completion:^(BOOL finished) { if (!view.isVisible) { view.hidden = YES; } }]; } #pragma mark - KVO Events - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([object conformsToProtocol:@protocol(SLKTypingIndicatorProtocol)] && [keyPath isEqualToString:@"visible"]) { [self slk_willShowOrHideTypeIndicatorView:object]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } #pragma mark - Auto-Completion Text Processing - (void)registerPrefixesForAutoCompletion:(NSArray *)prefixes { if (prefixes.count == 0) { return; } NSMutableSet *set = [NSMutableSet setWithSet:self.registeredPrefixes]; [set addObjectsFromArray:[prefixes copy]]; _registeredPrefixes = [NSSet setWithSet:set]; } - (BOOL)shouldProcessTextForAutoCompletion { if (!_registeredPrefixes || _registeredPrefixes.count == 0) { return NO; } return YES; } - (BOOL)shouldDisableTypingSuggestionForAutoCompletion { if (!_registeredPrefixes || _registeredPrefixes.count == 0) { return NO; } return YES; } - (void)didChangeAutoCompletionPrefix:(NSString *)prefix andWord:(NSString *)word { // No implementation here. Meant to be overriden in subclass. } - (void)showAutoCompletionView:(BOOL)show { // Reloads the tableview before showing/hiding if (show) { [_autoCompletionView reloadData]; } self.autoCompleting = show; // Toggles auto-correction if requiered [self slk_enableTypingSuggestionIfNeeded]; CGFloat viewHeight = show ? [self heightForAutoCompletionView] : 0.0; if (self.autoCompletionViewHC.constant == viewHeight) { return; } // If the auto-completion view height is bigger than the maximum height allows, it is reduce to that size. Default 140 pts. CGFloat maximumHeight = [self maximumHeightForAutoCompletionView]; if (viewHeight > maximumHeight) { viewHeight = maximumHeight; } CGFloat contentViewHeight = self.scrollViewHC.constant + self.autoCompletionViewHC.constant; // On iPhone, the auto-completion view can't extend beyond the content view height if (SLK_IS_IPHONE && viewHeight > contentViewHeight) { viewHeight = contentViewHeight; } self.autoCompletionViewHC.constant = viewHeight; [self.view slk_animateLayoutIfNeededWithBounce:self.bounces options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionLayoutSubviews|UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction animations:NULL]; } - (void)showAutoCompletionViewWithPrefix:(NSString *)prefix andWord:(NSString *)word prefixRange:(NSRange)prefixRange { if ([self.registeredPrefixes containsObject:prefix]) { _foundPrefix = prefix; _foundWord = word; _foundPrefixRange = prefixRange; [self didChangeAutoCompletionPrefix:self.foundPrefix andWord:self.foundWord]; [self showAutoCompletionView:YES]; } } - (void)acceptAutoCompletionWithString:(NSString *)string { [self acceptAutoCompletionWithString:string keepPrefix:YES]; } - (void)acceptAutoCompletionWithString:(NSString *)string keepPrefix:(BOOL)keepPrefix { if (string.length == 0) { return; } NSUInteger location = self.foundPrefixRange.location; if (keepPrefix) { location += self.foundPrefixRange.length; } NSUInteger length = self.foundWord.length; if (!keepPrefix) { length += self.foundPrefixRange.length; } NSRange range = NSMakeRange(location, length); NSRange insertionRange = [self.textView slk_insertText:string inRange:range]; self.textView.selectedRange = NSMakeRange(insertionRange.location, 0); [self.textView slk_scrollToCaretPositonAnimated:NO]; [self cancelAutoCompletion]; } - (void)cancelAutoCompletion { [self slk_invalidateAutoCompletion]; [self slk_hideAutoCompletionViewIfNeeded]; } - (void)slk_processTextForAutoCompletion { NSString *text = self.textView.text; if ((!self.isAutoCompleting && text.length == 0) || self.isTransitioning || ![self shouldProcessTextForAutoCompletion]) { return; } [self.textView lookForPrefixes:self.registeredPrefixes completion:^(NSString *prefix, NSString *word, NSRange wordRange) { if (prefix.length > 0 && word.length > 0) { // Captures the detected symbol prefix _foundPrefix = prefix; // Removes the found prefix, or not. _foundWord = [word substringFromIndex:prefix.length]; // Used later for replacing the detected range with a new string alias returned in -acceptAutoCompletionWithString: _foundPrefixRange = NSMakeRange(wordRange.location, prefix.length); [self slk_handleProcessedWord:word wordRange:wordRange]; } else { [self cancelAutoCompletion]; } }]; } - (void)slk_handleProcessedWord:(NSString *)word wordRange:(NSRange)wordRange { // Cancel auto-completion if the cursor is placed before the prefix if (self.textView.selectedRange.location <= self.foundPrefixRange.location) { return [self cancelAutoCompletion]; } if (self.foundPrefix.length > 0) { if (wordRange.length == 0 || wordRange.length != word.length) { return [self cancelAutoCompletion]; } if (word.length > 0) { // If the prefix is still contained in the word, cancels if ([self.foundWord rangeOfString:self.foundPrefix].location != NSNotFound) { return [self cancelAutoCompletion]; } } else { return [self cancelAutoCompletion]; } } else { return [self cancelAutoCompletion]; } [self didChangeAutoCompletionPrefix:self.foundPrefix andWord:self.foundWord]; } - (void)slk_invalidateAutoCompletion { _foundPrefix = nil; _foundWord = nil; _foundPrefixRange = NSMakeRange(0,0); [_autoCompletionView setContentOffset:CGPointZero]; } - (void)slk_hideAutoCompletionViewIfNeeded { if (self.isAutoCompleting) { [self showAutoCompletionView:NO]; } } #pragma mark - Text Caching - (NSString *)keyForTextCaching { // No implementation here. Meant to be overriden in subclass. return nil; } - (NSString *)slk_keyForPersistency { NSString *key = [self keyForTextCaching]; if (key == nil) { return nil; } return [NSString stringWithFormat:@"%@.%@", SLKTextViewControllerDomain, key]; } - (void)slk_reloadTextView { NSString *key = [self slk_keyForPersistency]; if (key == nil) { return; } NSAttributedString *cachedAttributedText = [[NSAttributedString alloc] initWithString:@""]; id obj = [[NSUserDefaults standardUserDefaults] objectForKey:key]; if (obj) { if ([obj isKindOfClass:[NSString class]]) { cachedAttributedText = [[NSAttributedString alloc] initWithString:obj]; } else if ([obj isKindOfClass:[NSData class]]) { cachedAttributedText = [NSKeyedUnarchiver unarchiveObjectWithData:obj]; } } if (self.textView.attributedText.length == 0 || cachedAttributedText.length > 0) { self.textView.attributedText = cachedAttributedText; } } - (void)cacheTextView { [self slk_cacheAttributedTextToDisk:self.textView.attributedText]; } - (void)clearCachedText { [self slk_cacheAttributedTextToDisk:nil]; } - (void)slk_cacheAttributedTextToDisk:(NSAttributedString *)attributedText { NSString *key = [self slk_keyForPersistency]; if (!key || key.length == 0) { return; } NSAttributedString *cachedAttributedText = [[NSAttributedString alloc] initWithString:@""]; id obj = [[NSUserDefaults standardUserDefaults] objectForKey:key]; if (obj) { if ([obj isKindOfClass:[NSString class]]) { cachedAttributedText = [[NSAttributedString alloc] initWithString:obj]; } else if ([obj isKindOfClass:[NSData class]]) { cachedAttributedText = [NSKeyedUnarchiver unarchiveObjectWithData:obj]; } } // Caches text only if its a valid string and not already cached if (attributedText.length > 0 && ![attributedText isEqualToAttributedString:cachedAttributedText]) { NSData *data = [NSKeyedArchiver archivedDataWithRootObject:attributedText]; [[NSUserDefaults standardUserDefaults] setObject:data forKey:key]; } // Clears cache only if it exists else if (attributedText.length == 0 && cachedAttributedText.length > 0) { [[NSUserDefaults standardUserDefaults] removeObjectForKey:key]; } else { // Skips so it doesn't hit 'synchronize' unnecessarily return; } [[NSUserDefaults standardUserDefaults] synchronize]; } - (void)slk_cacheTextToDisk:(NSString *)text { NSString *key = [self slk_keyForPersistency]; if (!key || key.length == 0) { return; } NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text]; [self slk_cacheAttributedTextToDisk:attributedText]; } + (void)clearAllCachedText { NSMutableArray *cachedKeys = [NSMutableArray new]; for (NSString *key in [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys]) { if ([key rangeOfString:SLKTextViewControllerDomain].location != NSNotFound) { [cachedKeys addObject:key]; } } if (cachedKeys.count == 0) { return; } for (NSString *cachedKey in cachedKeys) { [[NSUserDefaults standardUserDefaults] removeObjectForKey:cachedKey]; } [[NSUserDefaults standardUserDefaults] synchronize]; } #pragma mark - Customization - (void)registerClassForTextView:(Class)aClass { if (aClass == nil) { return; } NSAssert([aClass isSubclassOfClass:[SLKTextView class]], @"The registered class is invalid, it must be a subclass of SLKTextView."); self.textViewClass = aClass; } - (void)registerClassForTypingIndicatorView:(Class)aClass { if (aClass == nil) { return; } NSAssert([aClass isSubclassOfClass:[UIView class]], @"The registered class is invalid, it must be a subclass of UIView."); self.typingIndicatorViewClass = aClass; } #pragma mark - UITextViewDelegate Methods - (BOOL)textView:(SLKTextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { if (![textView isKindOfClass:[SLKTextView class]]) { return YES; } BOOL newWordInserted = ([text rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].location != NSNotFound); // Records text for undo for every new word if (newWordInserted) { [textView slk_prepareForUndo:@"Word Change"]; } // Detects double spacebar tapping, to replace the default "." insert with a formatting symbol, if needed. if (textView.isFormattingEnabled && range.location > 0 && text.length > 0 && [[NSCharacterSet whitespaceCharacterSet] characterIsMember:[text characterAtIndex:0]] && [[NSCharacterSet whitespaceCharacterSet] characterIsMember:[textView.text characterAtIndex:range.location - 1]]) { BOOL shouldChange = YES; // Since we are moving 2 characters to the left, we need for to make sure that the string's lenght, // before the caret position, is higher than 2. if ([textView.text substringToIndex:textView.selectedRange.location].length < 2) { return YES; } NSRange wordRange = range; wordRange.location -= 2; // minus the white space added with the double space bar tapping if (wordRange.location == NSNotFound) { return YES; } NSArray *symbols = textView.registeredSymbols; NSMutableCharacterSet *invalidCharacters = [NSMutableCharacterSet new]; [invalidCharacters formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; [invalidCharacters formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]]; [invalidCharacters removeCharactersInString:[symbols componentsJoinedByString:@""]]; for (NSString *symbol in symbols) { // Detects the closest registered symbol to the caret, from right to left NSRange searchRange = NSMakeRange(0, wordRange.location); NSRange prefixRange = [textView.text rangeOfString:symbol options:NSBackwardsSearch range:searchRange]; if (prefixRange.location == NSNotFound) { continue; } NSRange nextCharRange = NSMakeRange(prefixRange.location+1, 1); NSString *charAfterSymbol = [textView.text substringWithRange:nextCharRange]; if (prefixRange.location != NSNotFound && ![invalidCharacters characterIsMember:[charAfterSymbol characterAtIndex:0]]) { if ([self textView:textView shouldInsertSuffixForFormattingWithSymbol:symbol prefixRange:prefixRange]) { NSRange suffixRange; [textView wordAtRange:wordRange rangeInText:&suffixRange]; // Skip if the detected word already has a suffix if ([[textView.text substringWithRange:suffixRange] hasSuffix:symbol]) { continue; } suffixRange.location += suffixRange.length; suffixRange.length = 0; NSString *lastCharacter = [textView.text substringWithRange:NSMakeRange(suffixRange.location, 1)]; // Checks if the last character was a line break, so we append the symbol in the next line too if ([[NSCharacterSet newlineCharacterSet] characterIsMember:[lastCharacter characterAtIndex:0]]) { suffixRange.location += 1; } [textView slk_insertText:symbol inRange:suffixRange]; shouldChange = NO; // Reset the original cursor location +1 for the new character NSRange adjustedCursorPosition = NSMakeRange(range.location + 1, 0); textView.selectedRange = adjustedCursorPosition; break; // exit } } } return shouldChange; } else if ([text isEqualToString:@"\n"]) { //Detected break. Should insert new line break programatically instead. [textView slk_insertNewLineBreak]; return NO; } else { NSDictionary *userInfo = @{@"text": text, @"range": [NSValue valueWithRange:range]}; [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewTextWillChangeNotification object:self.textView userInfo:userInfo]; return YES; } } - (void)textViewDidChange:(SLKTextView *)textView { // Keep to avoid unnecessary crashes. Was meant to be overriden in subclass while calling super. } - (void)textViewDidChangeSelection:(SLKTextView *)textView { // Keep to avoid unnecessary crashes. Was meant to be overriden in subclass while calling super. } - (BOOL)textViewShouldBeginEditing:(SLKTextView *)textView { return YES; } - (BOOL)textViewShouldEndEditing:(SLKTextView *)textView { return YES; } - (void)textViewDidBeginEditing:(SLKTextView *)textView { // No implementation here. Meant to be overriden in subclass. } - (void)textViewDidEndEditing:(SLKTextView *)textView { // No implementation here. Meant to be overriden in subclass. } #pragma mark - SLKTextViewDelegate Methods - (BOOL)textView:(SLKTextView *)textView shouldOfferFormattingForSymbol:(NSString *)symbol { return YES; } - (BOOL)textView:(SLKTextView *)textView shouldInsertSuffixForFormattingWithSymbol:(NSString *)symbol prefixRange:(NSRange)prefixRange { if (prefixRange.location > 0) { NSRange previousCharRange = NSMakeRange(prefixRange.location-1, 1); NSString *previousCharacter = [self.textView.text substringWithRange:previousCharRange]; // Only insert a suffix if the character before the prefix was a whitespace or a line break if ([previousCharacter rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].location != NSNotFound) { return YES; } else { return NO; } } return YES; } #pragma mark - UITableViewDataSource Methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 0; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { return nil; } #pragma mark - UICollectionViewDataSource Methods - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section; { return 0; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { return nil; } #pragma mark - UIScrollViewDelegate Methods - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView { if (!self.scrollViewProxy.scrollsToTop || self.keyboardStatus == SLKKeyboardStatusWillShow) { return NO; } if (self.isInverted) { [self.scrollViewProxy slk_scrollToBottomAnimated:YES]; return NO; } else { return YES; } } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { self.movingKeyboard = NO; } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { self.movingKeyboard = NO; } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { if ([scrollView isEqual:_autoCompletionView]) { CGRect frame = self.autoCompletionHairline.frame; frame.origin.y = scrollView.contentOffset.y; self.autoCompletionHairline.frame = frame; } else { if (!self.isMovingKeyboard) { _scrollViewOffsetBeforeDragging = scrollView.contentOffset; _keyboardHeightBeforeDragging = self.keyboardHC.constant; } } } #pragma mark - UIGestureRecognizerDelegate Methods - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gesture { if ([gesture isEqual:self.singleTapGesture]) { return [self.textView isFirstResponder] && ![self ignoreTextInputbarAdjustment]; } else if ([gesture isEqual:self.verticalPanGesture]) { return self.keyboardPanningEnabled && ![self ignoreTextInputbarAdjustment]; } return NO; } #pragma mark - UIAlertViewDelegate Methods #ifndef __IPHONE_8_0 - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if (alertView.tag != kSLKAlertViewClearTextTag || buttonIndex == [alertView cancelButtonIndex] ) { return; } // Clears the text but doesn't clear the undo manager if (self.shakeToClearEnabled) { [self.textView slk_clearText:NO]; } } #endif #pragma mark - View Auto-Layout - (void)slk_setupViewConstraints { NSDictionary *views = @{@"scrollView": self.scrollViewProxy, @"autoCompletionView": self.autoCompletionView, @"typingIndicatorView": self.typingIndicatorProxyView, @"textInputbar": self.textInputbar }; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView(0@750)][typingIndicatorView(0)]-0@999-[textInputbar(0)]|" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=0)-[autoCompletionView(0@750)][typingIndicatorView]" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[autoCompletionView]|" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[typingIndicatorView]|" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[textInputbar]|" options:0 metrics:nil views:views]]; self.scrollViewHC = [self.view slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.scrollViewProxy secondItem:nil]; self.autoCompletionViewHC = [self.view slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.autoCompletionView secondItem:nil]; self.typingIndicatorViewHC = [self.view slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.typingIndicatorProxyView secondItem:nil]; self.textInputbarHC = [self.view slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.textInputbar secondItem:nil]; self.keyboardHC = [self.view slk_constraintForAttribute:NSLayoutAttributeBottom firstItem:self.view secondItem:self.textInputbar]; [self slk_updateViewConstraints]; } - (void)slk_updateViewConstraints { self.textInputbarHC.constant = self.textInputbar.hidden ? 0.0 : self.textInputbar.minimumInputbarHeight; self.scrollViewHC.constant = [self slk_appropriateScrollViewHeight]; self.keyboardHC.constant = [self slk_appropriateKeyboardHeightFromRect:CGRectNull]; if (_textInputbar.isEditing) { self.textInputbarHC.constant += self.textInputbar.editorContentViewHeight; } [super updateViewConstraints]; } #pragma mark - Keyboard Command registration - (void)slk_registerKeyCommands { __weak typeof(self) weakSelf = self; // Enter Key [self.textView observeKeyInput:@"\r" modifiers:0 title:NSLocalizedString(@"Send/Accept", nil) completion:^(UIKeyCommand *keyCommand) { [weakSelf didPressReturnKey:keyCommand]; }]; // Esc Key [self.textView observeKeyInput:UIKeyInputEscape modifiers:0 title:NSLocalizedString(@"Dismiss", nil) completion:^(UIKeyCommand *keyCommand) { [weakSelf didPressEscapeKey:keyCommand]; }]; // Up Arrow [self.textView observeKeyInput:UIKeyInputUpArrow modifiers:0 title:nil completion:^(UIKeyCommand *keyCommand) { [weakSelf didPressArrowKey:keyCommand]; }]; // Down Arrow [self.textView observeKeyInput:UIKeyInputDownArrow modifiers:0 title:nil completion:^(UIKeyCommand *keyCommand) { [weakSelf didPressArrowKey:keyCommand]; }]; } - (NSArray *)keyCommands { // Important to keep this in, for backwards compatibility. return @[]; } #pragma mark - NSNotificationCenter registration - (void)slk_registerNotifications { [self slk_unregisterNotifications]; NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; // Keyboard notifications [notificationCenter addObserver:self selector:@selector(slk_willShowOrHideKeyboard:) name:UIKeyboardWillShowNotification object:nil]; [notificationCenter addObserver:self selector:@selector(slk_willShowOrHideKeyboard:) name:UIKeyboardWillHideNotification object:nil]; [notificationCenter addObserver:self selector:@selector(slk_didShowOrHideKeyboard:) name:UIKeyboardDidShowNotification object:nil]; [notificationCenter addObserver:self selector:@selector(slk_didShowOrHideKeyboard:) name:UIKeyboardDidHideNotification object:nil]; #if SLK_KEYBOARD_NOTIFICATION_DEBUG [notificationCenter addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardWillShowNotification object:nil]; [notificationCenter addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardDidShowNotification object:nil]; [notificationCenter addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardWillHideNotification object:nil]; [notificationCenter addObserver:self selector:@selector(slk_didPostSLKKeyboardNotification:) name:SLKKeyboardDidHideNotification object:nil]; #endif // TextView notifications [notificationCenter addObserver:self selector:@selector(slk_willChangeTextViewText:) name:SLKTextViewTextWillChangeNotification object:nil]; [notificationCenter addObserver:self selector:@selector(slk_didChangeTextViewText:) name:UITextViewTextDidChangeNotification object:nil]; [notificationCenter addObserver:self selector:@selector(slk_didChangeTextViewContentSize:) name:SLKTextViewContentSizeDidChangeNotification object:nil]; [notificationCenter addObserver:self selector:@selector(slk_didChangeTextViewSelectedRange:) name:SLKTextViewSelectedRangeDidChangeNotification object:nil]; [notificationCenter addObserver:self selector:@selector(slk_didChangeTextViewPasteboard:) name:SLKTextViewDidPasteItemNotification object:nil]; [notificationCenter addObserver:self selector:@selector(slk_didShakeTextView:) name:SLKTextViewDidShakeNotification object:nil]; // Application notifications [notificationCenter addObserver:self selector:@selector(cacheTextView) name:UIApplicationWillTerminateNotification object:nil]; [notificationCenter addObserver:self selector:@selector(cacheTextView) name:UIApplicationDidEnterBackgroundNotification object:nil]; [notificationCenter addObserver:self selector:@selector(cacheTextView) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; } - (void)slk_unregisterNotifications { NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; // Keyboard notifications [notificationCenter removeObserver:self name:UIKeyboardWillShowNotification object:nil]; [notificationCenter removeObserver:self name:UIKeyboardWillHideNotification object:nil]; [notificationCenter removeObserver:self name:UIKeyboardDidShowNotification object:nil]; [notificationCenter removeObserver:self name:UIKeyboardDidHideNotification object:nil]; #if SLK_KEYBOARD_NOTIFICATION_DEBUG [notificationCenter removeObserver:self name:SLKKeyboardWillShowNotification object:nil]; [notificationCenter removeObserver:self name:SLKKeyboardDidShowNotification object:nil]; [notificationCenter removeObserver:self name:SLKKeyboardWillHideNotification object:nil]; [notificationCenter removeObserver:self name:SLKKeyboardDidHideNotification object:nil]; #endif // TextView notifications [notificationCenter removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil]; [notificationCenter removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil]; [notificationCenter removeObserver:self name:SLKTextViewTextWillChangeNotification object:nil]; [notificationCenter removeObserver:self name:UITextViewTextDidChangeNotification object:nil]; [notificationCenter removeObserver:self name:SLKTextViewContentSizeDidChangeNotification object:nil]; [notificationCenter removeObserver:self name:SLKTextViewSelectedRangeDidChangeNotification object:nil]; [notificationCenter removeObserver:self name:SLKTextViewDidPasteItemNotification object:nil]; [notificationCenter removeObserver:self name:SLKTextViewDidShakeNotification object:nil]; // Application notifications [notificationCenter removeObserver:self name:UIApplicationWillTerminateNotification object:nil]; [notificationCenter removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; [notificationCenter removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; } #pragma mark - View Auto-Rotation #ifdef __IPHONE_8_0 - (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id )coordinator { [super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator]; } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { [self slk_prepareForInterfaceTransitionWithDuration:coordinator.transitionDuration]; [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; } #else - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { if ([self respondsToSelector:@selector(viewWillTransitionToSize:withTransitionCoordinator:)]) { return; } [self slk_prepareForInterfaceTransitionWithDuration:duration]; } #endif #ifdef __IPHONE_9_0 - (UIInterfaceOrientationMask)supportedInterfaceOrientations #else - (NSUInteger)supportedInterfaceOrientations #endif { return UIInterfaceOrientationMaskAll; } - (BOOL)shouldAutorotate { return YES; } #pragma mark - View lifeterm - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)dealloc { [self slk_unregisterNotifications]; [_typingIndicatorProxyView removeObserver:self forKeyPath:@"visible"]; } @end ================================================ FILE: Source/SLKTypingIndicatorProtocol.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import NS_ASSUME_NONNULL_BEGIN /** Generic protocol needed when customizing your own typing indicator view. */ @protocol SLKTypingIndicatorProtocol @required /** Returns YES if the indicator is visible. SLKTextViewController depends on this property internally, by observing its value changes to update the typing indicator view's constraints automatically. You can simply @synthesize this property to make it KVO compliant, or override its setter method and wrap its implementation with -willChangeValueForKey: and -didChangeValueForKey: methods, for more complex KVO compliance. */ @property (nonatomic, getter = isVisible) BOOL visible; @optional /** Dismisses the indicator view. */ - (void)dismissIndicator; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/SLKTypingIndicatorView.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import #import "SLKTypingIndicatorProtocol.h" NS_ASSUME_NONNULL_BEGIN /** @name A custom view to display an indicator of users typing. */ @interface SLKTypingIndicatorView : UIView /** The amount of time a name should keep visible. If is zero, the indicator will not remove nor disappear automatically. Default is 6.0 seconds*/ @property (nonatomic, readwrite) NSTimeInterval interval; /** If YES, the user can dismiss the indicator by tapping on it. Default is NO. */ @property (nonatomic, readwrite) BOOL canResignByTouch; /** The color of the text. Default is grayColor. */ @property (nonatomic, strong) UIColor *textColor; /** The color of the highlighted text. Default is grayColor. */ @property (nonatomic, strong) UIColor *highlightTextColor; /** The font of the text. Default is system font, 12 pts. */ @property (nonatomic, strong) UIFont *textFont; /** The font to be used when matching a username string. Default is system bold font, 12 pts. */ @property (nonatomic, strong) UIFont *highlightFont; /** The inner padding to use when laying out content in the view. Default is {10, 40, 10, 10}. */ @property (nonatomic, assign) UIEdgeInsets contentInset; /** Inserts a user name, only if that user name is not yet on the list. Each inserted name has an attached timer, which will automatically remove the name from the list once the interval is reached (default 6 seconds). The control follows a set of display rules, to accomodate the screen size: - When only 1 user name is set, it will display ":name is typing" - When only 2 user names are set, it will display ":name & :name are typing" - When more than 2 user names are set, it will display "several people are typing" @param username The user name string. */ - (void)insertUsername:(NSString *_Nullable)username; /** Removes a user name, if existent on the list. Once there are no more items on the list, the indicator will automatically try to hide (by setting it self to visible = NO). @param username The user name string. */ - (void)removeUsername:(NSString *_Nullable)username; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/SLKTypingIndicatorView.m ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import "SLKTypingIndicatorView.h" #import "UIView+SLKAdditions.h" #import "SLKUIConstants.h" #define SLKTypingIndicatorViewIdentifier [NSString stringWithFormat:@"%@.%@", SLKTextViewControllerDomain, NSStringFromClass([self class])] @interface SLKTypingIndicatorView () // The text label used to display the typing indicator content. @property (nonatomic, strong) UILabel *textLabel; @property (nonatomic, strong) NSMutableArray *usernames; @property (nonatomic, strong) NSMutableArray *timers; // Auto-Layout margin constraints used for updating their constants @property (nonatomic, strong) NSLayoutConstraint *leftContraint; @property (nonatomic, strong) NSLayoutConstraint *rightContraint; @end @implementation SLKTypingIndicatorView @synthesize visible = _visible; #pragma mark - Initializer - (id)init { if (self = [super init]) { [self slk_commonInit]; } return self; } - (instancetype)initWithCoder:(NSCoder *)coder { if (self = [super initWithCoder:coder]) { [self slk_commonInit]; } return self; } - (void)slk_commonInit { self.backgroundColor = [UIColor whiteColor]; self.interval = 6.0; self.canResignByTouch = NO; self.usernames = [NSMutableArray new]; self.timers = [NSMutableArray new]; self.textColor = [UIColor grayColor]; self.highlightTextColor = [UIColor grayColor]; self.textFont = [UIFont systemFontOfSize:12.0]; self.highlightFont = [UIFont boldSystemFontOfSize:12.0]; self.contentInset = UIEdgeInsetsMake(10.0, 40.0, 10.0, 10.0); [self addSubview:self.textLabel]; [self slk_setupConstraints]; } #pragma mark - SLKTypingIndicatorProtocol - (void)setVisible:(BOOL)visible { // Skip when updating the same value, specially to avoid inovking KVO unnecessary if (self.isVisible == visible) { return; } // Required implementation for key-value observer compliance [self willChangeValueForKey:NSStringFromSelector(@selector(isVisible))]; _visible = visible; if (!visible) { [self slk_invalidateTimers]; } // Required implementation for key-value observer compliance [self didChangeValueForKey:NSStringFromSelector(@selector(isVisible))]; } - (void)dismissIndicator { if (self.isVisible) { self.visible = NO; } } #pragma mark - Getters - (UILabel *)textLabel { if (!_textLabel) { _textLabel = [UILabel new]; _textLabel.translatesAutoresizingMaskIntoConstraints = NO; _textLabel.backgroundColor = [UIColor clearColor]; _textLabel.contentMode = UIViewContentModeTopLeft; _textLabel.userInteractionEnabled = NO; } return _textLabel; } - (NSAttributedString *)attributedString { if (self.usernames.count == 0) { return nil; } NSString *text = @""; NSString *firstObject = [self.usernames firstObject]; NSString *lastObject = [self.usernames lastObject]; if (self.usernames.count == 1) { text = [NSString stringWithFormat:NSLocalizedString(@"%@ is typing", nil), firstObject]; } else if (self.usernames.count == 2) { text = [NSString stringWithFormat:NSLocalizedString(@"%@ & %@ are typing", nil), firstObject, lastObject]; } else if (self.usernames.count > 2) { text = NSLocalizedString(@"Several people are typing", nil); } NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; style.alignment = NSTextAlignmentLeft; style.lineBreakMode = NSLineBreakByTruncatingTail; style.minimumLineHeight = 10.0; NSDictionary *attributes = @{NSFontAttributeName: self.textFont, NSForegroundColorAttributeName: self.textColor, NSParagraphStyleAttributeName: style, }; NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:attributes]; if (self.usernames.count <= 2) { [attributedString addAttribute:NSFontAttributeName value:self.highlightFont range:[text rangeOfString:firstObject]]; [attributedString addAttribute:NSFontAttributeName value:self.highlightFont range:[text rangeOfString:lastObject]]; [attributedString addAttribute:NSForegroundColorAttributeName value:self.highlightTextColor range:[text rangeOfString:firstObject]]; [attributedString addAttribute:NSForegroundColorAttributeName value:self.highlightTextColor range:[text rangeOfString:lastObject]]; } return attributedString; } - (CGSize)intrinsicContentSize { return CGSizeMake(UIViewNoIntrinsicMetric, [self height]); } - (CGFloat)height { CGFloat height = self.textFont.lineHeight; height += self.contentInset.top; height += self.contentInset.bottom; return height; } #pragma mark - Setters - (void)setContentInset:(UIEdgeInsets)insets { if (UIEdgeInsetsEqualToEdgeInsets(self.contentInset, insets)) { return; } if (UIEdgeInsetsEqualToEdgeInsets(self.contentInset, UIEdgeInsetsZero)) { _contentInset = insets; return; } _contentInset = insets; [self slk_updateConstraintConstants]; } - (void)setHidden:(BOOL)hidden { if (self.isHidden == hidden) { return; } if (hidden) { [self slk_prepareForReuse]; } [super setHidden:hidden]; } #pragma mark - Public Methods - (void)insertUsername:(NSString *)username; { if (!username) { return; } BOOL isShowing = [self.usernames containsObject:username]; if (_interval > 0.0) { if (isShowing) { NSTimer *timer = [self slk_timerWithIdentifier:username]; [self slk_invalidateTimer:timer]; } NSTimer *timer = [NSTimer timerWithTimeInterval:_interval target:self selector:@selector(slk_shouldRemoveUsername:) userInfo:@{SLKTypingIndicatorViewIdentifier: username} repeats:NO]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; [self.timers addObject:timer]; } if (isShowing) { return; } [self.usernames addObject:username]; NSAttributedString *attributedString = [self attributedString]; self.textLabel.attributedText = attributedString; self.visible = YES; } - (void)removeUsername:(NSString *)username { if (!username || ![self.usernames containsObject:username]) { return; } [self.usernames removeObject:username]; if (self.usernames.count > 0) { self.textLabel.attributedText = [self attributedString]; } else { self.visible = NO; } } #pragma mark - Private Methods - (void)slk_shouldRemoveUsername:(NSTimer *)timer { NSString *identifier = [timer.userInfo objectForKey:SLKTypingIndicatorViewIdentifier]; [self removeUsername:identifier]; [self slk_invalidateTimer:timer]; } - (NSTimer *)slk_timerWithIdentifier:(NSString *)identifier { for (NSTimer *timer in self.timers) { if ([identifier isEqualToString:[timer.userInfo objectForKey:SLKTypingIndicatorViewIdentifier]]) { return timer; } } return nil; } - (void)slk_invalidateTimer:(NSTimer *)timer { if (timer) { [timer invalidate]; [self.timers removeObject:timer]; timer = nil; } } - (void)slk_invalidateTimers { for (NSTimer *timer in self.timers) { [timer invalidate]; } [self.timers removeAllObjects]; } - (void)slk_prepareForReuse { [self slk_invalidateTimers]; self.textLabel.text = nil; [self.usernames removeAllObjects]; } - (void)slk_setupConstraints { NSDictionary *views = @{@"textLabel": self.textLabel}; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[textLabel]|" options:0 metrics:nil views:views]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(0)-[textLabel]-(0@750)-|" options:0 metrics:nil views:views]]; self.leftContraint = [[self slk_constraintsForAttribute:NSLayoutAttributeLeading] firstObject]; self.rightContraint = [[self slk_constraintsForAttribute:NSLayoutAttributeTrailing] firstObject]; [self slk_updateConstraintConstants]; } - (void)slk_updateConstraintConstants { self.leftContraint.constant = self.contentInset.left; self.rightContraint.constant = self.contentInset.right; } #pragma mark - Hit Testing - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; if (self.canResignByTouch) { [self dismissIndicator]; } } #pragma mark - Lifeterm - (void)dealloc { [self slk_invalidateTimers]; } @end ================================================ FILE: Source/SLKUIConstants.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #define SLK_IS_LANDSCAPE ([[UIApplication sharedApplication] statusBarOrientation] == UIDeviceOrientationLandscapeLeft || [[UIApplication sharedApplication] statusBarOrientation] == UIDeviceOrientationLandscapeRight) #define SLK_IS_IPAD ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) #define SLK_IS_IPHONE ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) #define SLK_IS_IPHONE4 (SLK_IS_IPHONE && SLKKeyWindowBounds().size.height < 568.0) #define SLK_IS_IPHONE5 (SLK_IS_IPHONE && SLKKeyWindowBounds().size.height == 568.0) #define SLK_IS_IPHONE6 (SLK_IS_IPHONE && SLKKeyWindowBounds().size.height == 667.0) #define SLK_IS_IPHONE6PLUS (SLK_IS_IPHONE && SLKKeyWindowBounds().size.height == 736.0 || SLKKeyWindowBounds().size.width == 736.0) // Both orientations #define SLK_IS_IOS8_AND_HIGHER ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0) #define SLK_IS_IOS9_AND_HIGHER ([[UIDevice currentDevice].systemVersion floatValue] >= 9.0) #define SLK_KEYBOARD_NOTIFICATION_DEBUG DEBUG && 0 // Logs every keyboard notification being sent static NSString *SLKTextViewControllerDomain = @"com.slack.TextViewController"; /** Returns a constant font size difference reflecting the current accessibility settings. @param category A content size category constant string. @returns A float constant font size difference. */ __unused static CGFloat SLKPointSizeDifferenceForCategory(NSString *category) { if ([category isEqualToString:UIContentSizeCategoryExtraSmall]) return -3.0; if ([category isEqualToString:UIContentSizeCategorySmall]) return -2.0; if ([category isEqualToString:UIContentSizeCategoryMedium]) return -1.0; if ([category isEqualToString:UIContentSizeCategoryLarge]) return 0.0; if ([category isEqualToString:UIContentSizeCategoryExtraLarge]) return 2.0; if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge]) return 4.0; if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) return 6.0; if ([category isEqualToString:UIContentSizeCategoryAccessibilityMedium]) return 8.0; if ([category isEqualToString:UIContentSizeCategoryAccessibilityLarge]) return 10.0; if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) return 11.0; if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) return 12.0; if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) return 13.0; return 0; } __unused static CGRect SLKKeyWindowBounds() { return [[UIApplication sharedApplication] keyWindow].bounds; } __unused static CGRect SLKRectInvert(CGRect rect) { CGRect invert = CGRectZero; invert.origin.x = rect.origin.y; invert.origin.y = rect.origin.x; invert.size.width = rect.size.height; invert.size.height = rect.size.width; return invert; } ================================================ FILE: Source/UIResponder+SLKAdditions.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import /** @name UIResponder additional features used for SlackTextViewController. */ @interface UIResponder (SLKAdditions) /** Returns the current first responder object. @return A UIResponder instance. */ + (nullable instancetype)slk_currentFirstResponder; @end ================================================ FILE: Source/UIResponder+SLKAdditions.m ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import "UIResponder+SLKAdditions.h" static __weak id ___currentFirstResponder; @implementation UIResponder (SLKAdditions) /** Based on Jakob Egger's answer in http://stackoverflow.com/a/14135456/590010 */ + (instancetype)slk_currentFirstResponder { ___currentFirstResponder = nil; [[UIApplication sharedApplication] sendAction:@selector(slk_findFirstResponder:) to:nil from:nil forEvent:nil]; return ___currentFirstResponder; } - (void)slk_findFirstResponder:(id)sender { ___currentFirstResponder = self; } @end ================================================ FILE: Source/UIScrollView+SLKAdditions.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import /** @name UIScrollView additional features used for SlackTextViewController. */ @interface UIScrollView (SLKAdditions) /** YES if the scrollView's offset is at the very top. */ @property (nonatomic, readonly) BOOL slk_isAtTop; /** YES if the scrollView's offset is at the very bottom. */ @property (nonatomic, readonly) BOOL slk_isAtBottom; /** The visible area of the content size. */ @property (nonatomic, readonly) CGRect slk_visibleRect; /** Sets the content offset to the top. @param animated YES to animate the transition at a constant velocity to the new offset, NO to make the transition immediate. */ - (void)slk_scrollToTopAnimated:(BOOL)animated; /** Sets the content offset to the bottom. @param animated YES to animate the transition at a constant velocity to the new offset, NO to make the transition immediate. */ - (void)slk_scrollToBottomAnimated:(BOOL)animated; /** Stops scrolling, if it was scrolling. */ - (void)slk_stopScrolling; @end ================================================ FILE: Source/UIScrollView+SLKAdditions.m ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import "UIScrollView+SLKAdditions.h" @implementation UIScrollView (SLKAdditions) - (void)slk_scrollToTopAnimated:(BOOL)animated { if ([self slk_canScroll]) { [self setContentOffset:CGPointZero animated:animated]; } } - (void)slk_scrollToBottomAnimated:(BOOL)animated { if ([self slk_canScroll]) { [self setContentOffset:[self slk_bottomRect].origin animated:animated]; } } - (BOOL)slk_canScroll { if (self.contentSize.height > CGRectGetHeight(self.frame)) { return YES; } return NO; } - (BOOL)slk_isAtTop { return CGRectGetMinY([self slk_visibleRect]) <= CGRectGetMinY(self.bounds); } - (BOOL)slk_isAtBottom { return CGRectGetMaxY([self slk_visibleRect]) >= CGRectGetMaxY([self slk_bottomRect]); } - (CGRect)slk_visibleRect { CGRect visibleRect; visibleRect.origin = self.contentOffset; visibleRect.size = self.frame.size; return visibleRect; } - (CGRect)slk_bottomRect { return CGRectMake(0.0, self.contentSize.height - CGRectGetHeight(self.bounds), CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds)); } - (void)slk_stopScrolling { if (!self.isDragging) { return; } CGPoint offset = self.contentOffset; offset.y -= 1.0; [self setContentOffset:offset]; offset.y += 1.0; [self setContentOffset:offset]; } @end ================================================ FILE: Source/UIView+SLKAdditions.h ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import NS_ASSUME_NONNULL_BEGIN /** @name UIView additional features used for SlackTextViewController. */ @interface UIView (SLKAdditions) /** Animates the view's constraints by calling layoutIfNeeded. @param bounce YES if the animation should use spring damping and velocity to give a bouncy effect to animations. @param options A mask of options indicating how you want to perform the animations. @param animations An additional block for custom animations. */ - (void)slk_animateLayoutIfNeededWithBounce:(BOOL)bounce options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations; - (void)slk_animateLayoutIfNeededWithBounce:(BOOL)bounce options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion; /** Animates the view's constraints by calling layoutIfNeeded. @param duration The total duration of the animations, measured in seconds. @param bounce YES if the animation should use spring damping and velocity to give a bouncy effect to animations. @param options A mask of options indicating how you want to perform the animations. @param animations An additional block for custom animations. */ - (void)slk_animateLayoutIfNeededWithDuration:(NSTimeInterval)duration bounce:(BOOL)bounce options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion; /** Returns the view constraints matching a specific layout attribute (top, bottom, left, right, leading, trailing, etc.) @param attribute The layout attribute to use for searching. @return An array of matching constraints. */ - (nullable NSArray *)slk_constraintsForAttribute:(NSLayoutAttribute)attribute; /** Returns a layout constraint matching a specific layout attribute and relationship between 2 items, first and second items. @param attribute The layout attribute to use for searching. @param first The first item in the relationship. @param second The second item in the relationship. @return A layout constraint. */ - (nullable NSLayoutConstraint *)slk_constraintForAttribute:(NSLayoutAttribute)attribute firstItem:(id __nullable)first secondItem:(id __nullable)second; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/UIView+SLKAdditions.m ================================================ // // SlackTextViewController // https://github.com/slackhq/SlackTextViewController // // Copyright 2014-2016 Slack Technologies, Inc. // Licence: MIT-Licence // #import "UIView+SLKAdditions.h" #import "SLKUIConstants.h" @implementation UIView (SLKAdditions) - (void)slk_animateLayoutIfNeededWithBounce:(BOOL)bounce options:(UIViewAnimationOptions)options animations:(void (^)(void))animations { [self slk_animateLayoutIfNeededWithBounce:bounce options:options animations:animations completion:NULL]; } - (void)slk_animateLayoutIfNeededWithBounce:(BOOL)bounce options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { NSTimeInterval duration = bounce ? 0.65 : 0.2; [self slk_animateLayoutIfNeededWithDuration:duration bounce:bounce options:options animations:animations completion:completion]; } - (void)slk_animateLayoutIfNeededWithDuration:(NSTimeInterval)duration bounce:(BOOL)bounce options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { if (bounce) { [UIView animateWithDuration:duration delay:0.0 usingSpringWithDamping:0.7 initialSpringVelocity:0.7 options:options animations:^{ [self layoutIfNeeded]; if (animations) { animations(); } } completion:completion]; } else { [UIView animateWithDuration:duration delay:0.0 options:options animations:^{ [self layoutIfNeeded]; if (animations) { animations(); } } completion:completion]; } } - (NSArray *)slk_constraintsForAttribute:(NSLayoutAttribute)attribute { NSPredicate *predicate = [NSPredicate predicateWithFormat:@"firstAttribute = %d", attribute]; return [self.constraints filteredArrayUsingPredicate:predicate]; } - (NSLayoutConstraint *)slk_constraintForAttribute:(NSLayoutAttribute)attribute firstItem:(id)first secondItem:(id)second { NSPredicate *predicate = [NSPredicate predicateWithFormat:@"firstAttribute = %d AND firstItem = %@ AND secondItem = %@", attribute, first, second]; return [[self.constraints filteredArrayUsingPredicate:predicate] firstObject]; } @end