diff --git a/Demo/Base.lproj/LaunchScreen.storyboard b/Demo/Base.lproj/LaunchScreen.storyboard index 2e721e1..f22440f 100644 --- a/Demo/Base.lproj/LaunchScreen.storyboard +++ b/Demo/Base.lproj/LaunchScreen.storyboard @@ -1,7 +1,12 @@ - - + + + + + - + + + @@ -13,15 +18,21 @@ - + - - + + + + - + diff --git a/Demo/Base.lproj/Main.storyboard b/Demo/Base.lproj/Main.storyboard index f56d2f3..e811923 100644 --- a/Demo/Base.lproj/Main.storyboard +++ b/Demo/Base.lproj/Main.storyboard @@ -1,25 +1,91 @@ - - + + + + + - + + + + - + - + - + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/ViewController.m b/Demo/ViewController.m index 7c6ae92..f70eddd 100644 --- a/Demo/ViewController.m +++ b/Demo/ViewController.m @@ -23,21 +23,38 @@ #import "ViewController.h" #import +@interface ViewController() + +@property (weak, nonatomic) IBOutlet UITextView *textView; + + +@end + @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; - UITextView *textView = [[UITextView alloc] init]; - textView.frame = CGRectMake(0, 20, CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds)); - textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - textView.placeholder = @"Are you sure you don\'t want to reconsider? Could you tell us why you wish to leave StyleShare? Your opinion helps us improve StyleShare into a better place for fashionistas from all around the world. We are always listening to our users. Help us improve!"; + self.textView.placeholder = @"Are you sure you don\'t want to reconsider? Could you tell us why you wish to leave StyleShare? Your opinion helps us improve StyleShare into a better place for fashionistas from all around the world. We are always listening to our users. Help us improve!"; + self.textView.placeholderLabel.textAlignment = NSTextAlignmentCenter; NSDictionary *attrs = @ { NSFontAttributeName: [UIFont boldSystemFontOfSize:20], }; - textView.attributedText = [[NSAttributedString alloc] initWithString:@"Hi" attributes:attrs]; - textView.font = [UIFont systemFontOfSize:15]; - [self.view addSubview:textView]; + self.textView.attributedText = [[NSAttributedString alloc] initWithString:@"Hi" attributes:attrs]; + self.textView.font = [UIFont systemFontOfSize:15]; + [self.view addSubview:self.textView]; +} + +- (IBAction)onTextRemove:(id)sender { + [self.textView removeFromSuperview]; +} + +- (IBAction)onChangeFont:(id)sender { + self.textView.font = [UIFont systemFontOfSize:30]; +} + +- (IBAction)onChangeColor:(id)sender { + self.textView.textColor = [UIColor redColor]; } @end diff --git a/Sources/UITextView+Placeholder.m b/Sources/UITextView+Placeholder.m index 276a0bf..8be400c 100644 --- a/Sources/UITextView+Placeholder.m +++ b/Sources/UITextView+Placeholder.m @@ -23,32 +23,89 @@ #import #import "UITextView+Placeholder.h" -@implementation UITextView (Placeholder) +#pragma mark - `observingKeys` -#pragma mark - Swizzle Dealloc +@interface DWTextViewEventObserver : NSObject + +@property (copy, nonatomic) void (^execution)(BOOL fontChanged); +// can't use weak, because weak before UITextView object dealloc, it will set all weak reference to nil +// it will make our removeObserver failed to remove. +@property (unsafe_unretained, nonatomic) UITextView *target; + +@end + +@implementation DWTextViewEventObserver + ++ (void)observe:(UITextView *)target then:(void (^)(BOOL fontChanged))exec { + DWTextViewEventObserver *monitor = [[DWTextViewEventObserver alloc] init]; + monitor.execution = exec; + monitor.target = target; + [monitor observeEvents]; + int randomKey; + // It is true that swizzle method of dealloc in NSObject Category can do the same thing, but that will cause method polluted! + objc_setAssociatedObject(target, &randomKey, monitor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} -+ (void)load { - // is this the best solution? - method_exchangeImplementations(class_getInstanceMethod(self.class, NSSelectorFromString(@"dealloc")), - class_getInstanceMethod(self.class, @selector(swizzledDealloc))); +#pragma mark - LifeCycle ++ (NSArray *)observingKeys { + return @[@"attributedText", + @"bounds", + @"font", + @"frame", + @"text", + @"textAlignment", + @"textContainerInset"]; } -- (void)swizzledDealloc { +- (void)dealloc { + [self releaseObserveKeys]; +} + +- (void)observeEvents { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updatePlaceholderLabel) + name:UITextViewTextDidChangeNotification + object:self.target]; + + for (NSString *key in self.class.observingKeys) { + [self.target addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil]; + } +} + +- (void)updatePlaceholderLabel { + if (self.execution) { + self.execution(NO); + } +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + if (self.execution) { + BOOL updateFont = [keyPath isEqualToString:@"font"] && change[NSKeyValueChangeNewKey] != nil; + self.execution(updateFont); + } +} + +- (void)releaseObserveKeys { [[NSNotificationCenter defaultCenter] removeObserver:self]; - UILabel *label = objc_getAssociatedObject(self, @selector(placeholderLabel)); - if (label) { - for (NSString *key in self.class.observingKeys) { - @try { - [self removeObserver:self forKeyPath:key]; - } - @catch (NSException *exception) { - // Do nothing - } + for (NSString *key in self.class.observingKeys) { + @try { + [self.target removeObserver:self forKeyPath:key context:nil]; + } + @catch (NSException *exception) { + // Do nothing } } - [self swizzledDealloc]; } +@end + +@implementation UITextView (Placeholder) + +#pragma mark - Swizzle Dealloc #pragma mark - Class Methods #pragma mark `defaultPlaceholderColor` @@ -64,20 +121,6 @@ + (UIColor *)defaultPlaceholderColor { return color; } - -#pragma mark - `observingKeys` - -+ (NSArray *)observingKeys { - return @[@"attributedText", - @"bounds", - @"font", - @"frame", - @"text", - @"textAlignment", - @"textContainerInset"]; -} - - #pragma mark - Properties #pragma mark `placeholderLabel` @@ -98,14 +141,13 @@ - (UILabel *)placeholderLabel { [self updatePlaceholderLabel]; self.needsUpdateFont = NO; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(updatePlaceholderLabel) - name:UITextViewTextDidChangeNotification - object:self]; - - for (NSString *key in self.class.observingKeys) { - [self addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil]; - } + __weak typeof(self) weakSelf = self; + [DWTextViewEventObserver observe:self then:^(BOOL fontChanged) { + if (fontChanged) { + weakSelf.needsUpdateFont = fontChanged; + } + [weakSelf updatePlaceholderLabel]; + }]; } return label; } @@ -141,7 +183,6 @@ - (void)setPlaceholderColor:(UIColor *)placeholderColor { self.placeholderLabel.textColor = placeholderColor; } - #pragma mark `needsUpdateFont` - (BOOL)needsUpdateFont { @@ -153,21 +194,7 @@ - (void)setNeedsUpdateFont:(BOOL)needsUpdate { } -#pragma mark - KVO - -- (void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(id)object - change:(NSDictionary *)change - context:(void *)context { - if ([keyPath isEqualToString:@"font"]) { - self.needsUpdateFont = (change[NSKeyValueChangeNewKey] != nil); - } - [self updatePlaceholderLabel]; -} - - #pragma mark - Update - - (void)updatePlaceholderLabel { if (self.text.length) { [self.placeholderLabel removeFromSuperview]; @@ -180,6 +207,7 @@ - (void)updatePlaceholderLabel { self.placeholderLabel.font = self.font; self.needsUpdateFont = NO; } + self.placeholderLabel.textAlignment = self.textAlignment; // `NSTextContainer` is available since iOS 7 diff --git a/Tests/Tests.swift b/Tests/Tests.swift index 2ae6d46..f2678d0 100644 --- a/Tests/Tests.swift +++ b/Tests/Tests.swift @@ -19,66 +19,85 @@ // 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. - import XCTest @testable import UITextView_Placeholder class Tests: XCTestCase { - + var textView: UITextView! - + // MARK: Setup - override func setUp() { super.setUp() self.textView = UITextView() } - - + + // MARK: Basic Tests - func testPlaceholder() { self.textView.placeholder = "Hello" XCTAssertEqual(self.textView.placeholderLabel.text, "Hello") self.textView.placeholder = nil XCTAssertEqual(self.textView.placeholderLabel.text, nil) + self.textView = nil; } - + + func testRelease() { + self.textView.placeholder = "Hello" + XCTAssertEqual(self.textView.placeholderLabel.text, "Hello") + self.textView.placeholder = nil + XCTAssertEqual(self.textView.placeholderLabel.text, nil) + + self.textView = nil + } + + func testNotification() { + self.textView.text = "SomeText" + self.textView.placeholder = "Hello"; + XCTAssertNil(self.textView.placeholderLabel.superview); + self.textView.selectAll(nil); + self.textView .replaceRange(self.textView.selectedTextRange!, withText: ""); + XCTAssertNotNil(self.textView.placeholderLabel.superview); + + self.textView.selectAll(nil); + self.textView .replaceRange(self.textView.selectedTextRange!, withText: "OtherText"); + XCTAssertNil(self.textView.placeholderLabel.superview); + } + func testAttributedPlaceholder() { let attributedPlaceholder = attributedString("Hello", .Bold(26)) self.textView.attributedPlaceholder = attributedPlaceholder XCTAssertEqual(self.textView.attributedPlaceholder, attributedPlaceholder) } - - + + // MARK: Fonts - func testSetFont_beforePlaceholder() { self.textView.font = UIFont.systemFontOfSize(34) self.textView.placeholder = "Hello" XCTAssertEqual(self.textView.placeholderLabel.text, "Hello") XCTAssertEqual(self.textView.placeholderLabel.font, UIFont.systemFontOfSize(34)) } - + func testSetFont_afterPlaceholder() { self.textView.placeholder = "Hello" self.textView.font = UIFont.systemFontOfSize(34) XCTAssertEqual(self.textView.placeholderLabel.text, "Hello") XCTAssertEqual(self.textView.placeholderLabel.font, UIFont.systemFontOfSize(34)) } - + func testSetFont_beforeAttributedPlaceholder() { let attributedPlaceholder = attributedString("Hello", .Bold(26)) self.textView.font = UIFont.systemFontOfSize(34) self.textView.attributedPlaceholder = attributedPlaceholder XCTAssertEqual(self.textView.attributedPlaceholder, attributedPlaceholder) } - + func testSetFont_afterAttributedPlaceholderFont() { let attributedPlaceholder = attributedString("Hello", .Bold(26)) self.textView.attributedPlaceholder = attributedPlaceholder self.textView.font = UIFont.systemFontOfSize(34) XCTAssertEqual(self.textView.attributedPlaceholder, attributedString("Hello", .Normal(34))) } - + } diff --git a/Tests/Utils.swift b/Tests/Utils.swift index c94a841..c9c63d1 100644 --- a/Tests/Utils.swift +++ b/Tests/Utils.swift @@ -19,13 +19,12 @@ // 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. - import UIKit enum Font { case Normal(CGFloat) case Bold(CGFloat) - + var font: UIFont { switch self { case .Normal(let size): return UIFont.systemFontOfSize(size) diff --git a/UITextView+Placeholder.xcodeproj/project.pbxproj b/UITextView+Placeholder.xcodeproj/project.pbxproj index 776a783..3f27905 100644 --- a/UITextView+Placeholder.xcodeproj/project.pbxproj +++ b/UITextView+Placeholder.xcodeproj/project.pbxproj @@ -247,17 +247,20 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0730; + LastUpgradeCheck = 0810; ORGANIZATIONNAME = "Suyeol Jeon"; TargetAttributes = { 0309F8AC1CBCC7FF00062D3B = { CreatedOnToolsVersion = 7.3; + LastSwiftMigration = 0810; }; 0309F8C51CBCC8A700062D3B = { CreatedOnToolsVersion = 7.3; }; 0309F8F41CBCCD6500062D3B = { CreatedOnToolsVersion = 7.3; + LastSwiftMigration = 0810; + ProvisioningStyle = Automatic; }; }; }; @@ -385,8 +388,10 @@ 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_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -409,7 +414,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -434,8 +439,10 @@ 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_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -452,9 +459,10 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -465,6 +473,7 @@ 0309F8B61CBCC7FF00062D3B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -477,12 +486,14 @@ PRODUCT_BUNDLE_IDENTIFIER = "kr.xoul.UITextView-Placeholder"; PRODUCT_NAME = UITextView_Placeholder; SKIP_INSTALL = YES; + SWIFT_VERSION = 2.3; }; name = Debug; }; 0309F8B71CBCC7FF00062D3B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -495,6 +506,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "kr.xoul.UITextView-Placeholder"; PRODUCT_NAME = UITextView_Placeholder; SKIP_INSTALL = YES; + SWIFT_VERSION = 2.3; }; name = Release; }; @@ -525,22 +537,28 @@ 0309F8FD1CBCCD6500062D3B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "Supporting Files/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 9.3; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = kr.xoul.Tests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 2.3; }; name = Debug; }; 0309F8FE1CBCCD6500062D3B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "Supporting Files/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 9.3; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = kr.xoul.Tests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 2.3; }; name = Release; }; diff --git a/UITextView+Placeholder.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme b/UITextView+Placeholder.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme index 835af73..bac4dca 100644 --- a/UITextView+Placeholder.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme +++ b/UITextView+Placeholder.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme @@ -1,6 +1,6 @@