UITextView+Placeholder.m 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. #import "UITextView+Placeholder.h"
  2. #import <objc/runtime.h>
  3. @implementation UITextView (Placeholder)
  4. #pragma mark - Swizzle Dealloc
  5. + (void)load {
  6. // is this the best solution?
  7. method_exchangeImplementations(class_getInstanceMethod(self.class, NSSelectorFromString(@"dealloc")),
  8. class_getInstanceMethod(self.class, @selector(swizzledDealloc)));
  9. }
  10. - (void)swizzledDealloc {
  11. [[NSNotificationCenter defaultCenter] removeObserver:self];
  12. UILabel *label = objc_getAssociatedObject(self, @selector(placeholderLabel));
  13. if (label) {
  14. for (NSString *key in self.class.observingKeys) {
  15. @try {
  16. [self removeObserver:self forKeyPath:key];
  17. }
  18. @catch (NSException *exception) {
  19. // Do nothing
  20. }
  21. }
  22. }
  23. [self swizzledDealloc];
  24. }
  25. #pragma mark - Class Methods
  26. #pragma mark `defaultPlaceholderColor`
  27. + (UIColor *)defaultPlaceholderColor {
  28. static UIColor *color = nil;
  29. static dispatch_once_t onceToken;
  30. dispatch_once(&onceToken, ^{
  31. UITextField *textField = [[UITextField alloc] init];
  32. textField.placeholder = @" ";
  33. color = [textField valueForKeyPath:@"_placeholderLabel.textColor"];
  34. });
  35. return color;
  36. }
  37. #pragma mark - `observingKeys`
  38. + (NSArray *)observingKeys {
  39. return @[@"attributedText",
  40. @"bounds",
  41. @"font",
  42. @"frame",
  43. @"text",
  44. @"textAlignment",
  45. @"textContainerInset"];
  46. }
  47. #pragma mark - Properties
  48. #pragma mark `placeholderLabel`
  49. - (UILabel *)placeholderLabel {
  50. UILabel *label = objc_getAssociatedObject(self, @selector(placeholderLabel));
  51. if (!label) {
  52. NSAttributedString *originalText = self.attributedText;
  53. self.text = @" "; // lazily set font of `UITextView`.
  54. self.attributedText = originalText;
  55. label = [[UILabel alloc] init];
  56. label.textColor = [self.class defaultPlaceholderColor];
  57. label.numberOfLines = 0;
  58. label.userInteractionEnabled = NO;
  59. objc_setAssociatedObject(self, @selector(placeholderLabel), label, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  60. self.needsUpdateFont = YES;
  61. [self updatePlaceholderLabel];
  62. self.needsUpdateFont = NO;
  63. [[NSNotificationCenter defaultCenter] addObserver:self
  64. selector:@selector(updatePlaceholderLabel)
  65. name:UITextViewTextDidChangeNotification
  66. object:self];
  67. for (NSString *key in self.class.observingKeys) {
  68. [self addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil];
  69. }
  70. }
  71. return label;
  72. }
  73. #pragma mark `placeholder`
  74. - (NSString *)placeholder {
  75. return self.placeholderLabel.text;
  76. }
  77. - (void)setPlaceholder:(NSString *)placeholder {
  78. self.placeholderLabel.text = placeholder;
  79. [self updatePlaceholderLabel];
  80. }
  81. - (NSAttributedString *)attributedPlaceholder {
  82. return self.placeholderLabel.attributedText;
  83. }
  84. - (void)setAttributedPlaceholder:(NSAttributedString *)attributedPlaceholder {
  85. self.placeholderLabel.attributedText = attributedPlaceholder;
  86. [self updatePlaceholderLabel];
  87. }
  88. #pragma mark `placeholderColor`
  89. - (UIColor *)placeholderColor {
  90. return self.placeholderLabel.textColor;
  91. }
  92. - (void)setPlaceholderColor:(UIColor *)placeholderColor {
  93. self.placeholderLabel.textColor = placeholderColor;
  94. }
  95. #pragma mark `needsUpdateFont`
  96. - (BOOL)needsUpdateFont {
  97. return [objc_getAssociatedObject(self, @selector(needsUpdateFont)) boolValue];
  98. }
  99. - (void)setNeedsUpdateFont:(BOOL)needsUpdate {
  100. objc_setAssociatedObject(self, @selector(needsUpdateFont), @(needsUpdate), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  101. }
  102. #pragma mark - KVO
  103. - (void)observeValueForKeyPath:(NSString *)keyPath
  104. ofObject:(id)object
  105. change:(NSDictionary *)change
  106. context:(void *)context {
  107. if ([keyPath isEqualToString:@"font"]) {
  108. self.needsUpdateFont = (change[NSKeyValueChangeNewKey] != nil);
  109. }
  110. [self updatePlaceholderLabel];
  111. }
  112. #pragma mark - Update
  113. - (void)updatePlaceholderLabel {
  114. if (self.text.length) {
  115. [self.placeholderLabel removeFromSuperview];
  116. return;
  117. }
  118. [self insertSubview:self.placeholderLabel atIndex:0];
  119. if (self.needsUpdateFont) {
  120. self.placeholderLabel.font = self.font;
  121. self.needsUpdateFont = NO;
  122. }
  123. self.placeholderLabel.textAlignment = self.textAlignment;
  124. // `NSTextContainer` is available since iOS 7
  125. CGFloat lineFragmentPadding;
  126. UIEdgeInsets textContainerInset;
  127. #pragma deploymate push "ignored-api-availability"
  128. // iOS 7+
  129. if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
  130. lineFragmentPadding = self.textContainer.lineFragmentPadding;
  131. textContainerInset = self.textContainerInset;
  132. }
  133. #pragma deploymate pop
  134. // iOS 6
  135. else {
  136. lineFragmentPadding = 5;
  137. textContainerInset = UIEdgeInsetsMake(8, 0, 8, 0);
  138. }
  139. CGFloat x = lineFragmentPadding + textContainerInset.left;
  140. CGFloat y = textContainerInset.top;
  141. CGFloat width = CGRectGetWidth(self.bounds) - x - lineFragmentPadding - textContainerInset.right;
  142. CGFloat height = [self.placeholderLabel sizeThatFits:CGSizeMake(width, 0)].height;
  143. self.placeholderLabel.frame = CGRectMake(x, y, width, height);
  144. }
  145. @end