UITextView+Placeholder.m 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. // The MIT License (MIT)
  2. //
  3. // Copyright (c) 2014 Suyeol Jeon (http:xoul.kr)
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in all
  13. // copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. // SOFTWARE.
  22. #import <objc/runtime.h>
  23. #import "UITextView+Placeholder.h"
  24. @implementation UITextView (Placeholder)
  25. #pragma mark - Swizzle Dealloc
  26. + (void)load {
  27. // is this the best solution?
  28. method_exchangeImplementations(class_getInstanceMethod(self.class, NSSelectorFromString(@"dealloc")),
  29. class_getInstanceMethod(self.class, @selector(swizzledDealloc)));
  30. }
  31. - (void)swizzledDealloc {
  32. [[NSNotificationCenter defaultCenter] removeObserver:self];
  33. UITextView *textView = objc_getAssociatedObject(self, @selector(placeholderTextView));
  34. if (textView) {
  35. for (NSString *key in self.class.observingKeys) {
  36. @try {
  37. [self removeObserver:self forKeyPath:key];
  38. }
  39. @catch (NSException *exception) {
  40. // Do nothing
  41. }
  42. }
  43. }
  44. [self swizzledDealloc];
  45. }
  46. #pragma mark - Class Methods
  47. #pragma mark `defaultPlaceholderColor`
  48. + (UIColor *)defaultPlaceholderColor {
  49. static UIColor *color = nil;
  50. static dispatch_once_t onceToken;
  51. dispatch_once(&onceToken, ^{
  52. UITextField *textField = [[UITextField alloc] init];
  53. textField.placeholder = @" ";
  54. color = [UIColor grayColor];
  55. });
  56. return color;
  57. }
  58. #pragma mark - `observingKeys`
  59. + (NSArray *)observingKeys {
  60. return @[@"attributedText",
  61. @"bounds",
  62. @"font",
  63. @"frame",
  64. @"text",
  65. @"textAlignment",
  66. @"textContainerInset",
  67. @"textContainer.exclusionPaths"];
  68. }
  69. #pragma mark - Properties
  70. #pragma mark `placeholderTextView`
  71. - (UITextView *)placeholderTextView {
  72. UITextView *textView = objc_getAssociatedObject(self, @selector(placeholderTextView));
  73. if (!textView) {
  74. NSAttributedString *originalText = self.attributedText;
  75. self.text = @" "; // lazily set font of `UITextView`.
  76. self.attributedText = originalText;
  77. textView = [[UITextView alloc] init];
  78. textView.textColor = [self.class defaultPlaceholderColor];
  79. textView.userInteractionEnabled = NO;
  80. objc_setAssociatedObject(self, @selector(placeholderTextView), textView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  81. self.needsUpdateFont = YES;
  82. [self updatePlaceholderTextView];
  83. self.needsUpdateFont = NO;
  84. [[NSNotificationCenter defaultCenter] addObserver:self
  85. selector:@selector(updatePlaceholderTextView)
  86. name:UITextViewTextDidChangeNotification
  87. object:self];
  88. for (NSString *key in self.class.observingKeys) {
  89. [self addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil];
  90. }
  91. }
  92. return textView;
  93. }
  94. #pragma mark `placeholder`
  95. - (NSString *)placeholder {
  96. return self.placeholderTextView.text;
  97. }
  98. - (void)setPlaceholder:(NSString *)placeholder {
  99. self.placeholderTextView.text = placeholder;
  100. [self updatePlaceholderTextView];
  101. }
  102. - (NSAttributedString *)attributedPlaceholder {
  103. return self.placeholderTextView.attributedText;
  104. }
  105. - (void)setAttributedPlaceholder:(NSAttributedString *)attributedPlaceholder {
  106. self.placeholderTextView.attributedText = attributedPlaceholder;
  107. [self updatePlaceholderTextView];
  108. }
  109. #pragma mark `placeholderColor`
  110. - (UIColor *)placeholderColor {
  111. return self.placeholderTextView.textColor;
  112. }
  113. - (void)setPlaceholderColor:(UIColor *)placeholderColor {
  114. self.placeholderTextView.textColor = placeholderColor;
  115. }
  116. #pragma mark `needsUpdateFont`
  117. - (BOOL)needsUpdateFont {
  118. return [objc_getAssociatedObject(self, @selector(needsUpdateFont)) boolValue];
  119. }
  120. - (void)setNeedsUpdateFont:(BOOL)needsUpdate {
  121. objc_setAssociatedObject(self, @selector(needsUpdateFont), @(needsUpdate), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  122. }
  123. #pragma mark - KVO
  124. - (void)observeValueForKeyPath:(NSString *)keyPath
  125. ofObject:(id)object
  126. change:(NSDictionary *)change
  127. context:(void *)context {
  128. if ([keyPath isEqualToString:@"font"]) {
  129. self.needsUpdateFont = (change[NSKeyValueChangeNewKey] != nil);
  130. }
  131. [self updatePlaceholderTextView];
  132. }
  133. #pragma mark - Update
  134. - (void)updatePlaceholderTextView {
  135. if (self.text.length) {
  136. [self.placeholderTextView removeFromSuperview];
  137. } else {
  138. [self insertSubview:self.placeholderTextView atIndex:0];
  139. }
  140. if (self.needsUpdateFont) {
  141. self.placeholderTextView.font = self.font;
  142. self.needsUpdateFont = NO;
  143. }
  144. self.placeholderTextView.textAlignment = self.textAlignment;
  145. // `NSTextContainer` is available since iOS 7
  146. CGFloat lineFragmentPadding;
  147. UIEdgeInsets textContainerInset;
  148. #pragma deploymate push "ignored-api-availability"
  149. // iOS 7+
  150. if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
  151. lineFragmentPadding = self.textContainer.lineFragmentPadding;
  152. textContainerInset = self.textContainerInset;
  153. }
  154. #pragma deploymate pop
  155. // iOS 6
  156. else {
  157. lineFragmentPadding = 5;
  158. textContainerInset = UIEdgeInsetsMake(8, 0, 8, 0);
  159. }
  160. self.placeholderTextView.textContainer.exclusionPaths = self.textContainer.exclusionPaths;
  161. self.placeholderTextView.textContainerInset = self.textContainerInset;
  162. self.placeholderTextView.frame = self.bounds;
  163. }
  164. @end