How can you create a UITextField very similar to the one apple uses in their messaging app, Instagram uses, edmodo uses, Whatsapp uses and many other message sharing applications? This text field should animate up, animate down, be able to expand, only reach a certain height, and have other properties as well.
I researched for a while, but couldn't find an answer so I just created my own way to do it. There are several things you must do. First of all, this does use a UITextView and not a UITextField. Secondly, this code was written specifically for the iPhone 5, so you may need to play around with the coordinates. It does not use sizing classes or constraints. 1You need to implement the first. Your .h file should look like the following:
int returnPressed = 0;
int newLine;
#interface ViewController : UIViewController <UITextViewDelegate>
{
IBOutlet UIView *dock;
IBOutlet UIButton *sendButton;
IBOutlet UITextView *textView1;
CGRect previousRect;
}
#end
In the .m file there are many needs that need to be met to make it look how the iPhone message app looks. You'll notice how we have created our own custom place holder in here because textview don't really have them. Here is the .m file:
- (void)viewDidLoad {
[super viewDidLoad];
previousRect = CGRectZero;
textView1.delegate = self;
self->textView1.layer.borderWidth = 1.0f;
self->textView1.layer.borderColor = [[UIColor lightGrayColor] CGColor];
self->textView1.layer.cornerRadius = 6;
self->textView1.textColor = [UIColor lightGrayColor];
self->textView1.text = #"Place Holder";
}
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
if ([text isEqualToString:#"\n"]) {
returnPressed +=1;
if(returnPressed < 17){
textView1.frame = CGRectMake(8, 8, textView1.frame.size.width, textView1.frame.size.height + 17);
newLine = 17*returnPressed;
[UIView animateWithDuration:0.1 animations:^
{
dock.transform = CGAffineTransformMakeTranslation(0, -250 - newLine);
}
];
}
}
return YES;
}
- (void)textViewDidChange:(UITextView *)textView{
UITextPosition* pos = textView1.endOfDocument;
CGRect currentRect = [textView1 caretRectForPosition:pos];
if (currentRect.origin.y > previousRect.origin.y || [textView1.text isEqualToString:#"\n"]){
returnPressed +=1;
if(returnPressed < 17 && returnPressed > 1){
textView1.frame = CGRectMake(8, 8, textView1.frame.size.width, textView1.frame.size.height + 17);
newLine = 17*returnPressed;
[UIView animateWithDuration:0.1 animations:^
{
dock.transform = CGAffineTransformMakeTranslation(0, -250 - newLine);
}
];
}
}
previousRect = currentRect;
}
- (BOOL)textViewShouldBeginEditing:(UITextView *)textField {
if([textView1.text isEqualToString:#""] || [textView1.text isEqualToString:#"Place Holder"]){
textView1.text = #"";
}
textView1.textColor = [UIColor blackColor];
[UIView animateWithDuration:0.209 animations:^
{
dock.transform = CGAffineTransformMakeTranslation(0, -250 - newLine);
}
completion:^(BOOL finished){}];
return YES;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch * touch = [touches anyObject];
if(touch.phase == UITouchPhaseBegan) {
[textView1 resignFirstResponder];
[self.view endEditing:YES];
int height = returnPressed*20;
[UIView animateWithDuration:0.209 animations:^
{
dock.transform = CGAffineTransformMakeTranslation(0, -height);
}];
if([textView1.text isEqualToString:#""]){
self->textView1.textColor = [UIColor lightGrayColor];
self->textView1.text = #"Place Holder";
}
}
}
That is everything. I hope this helps anyone who was trying to do this and I hope you can integrate this into an app of yours.
There is a pod that you can use for growing text view's :
https://cocoapods.org/pods/HPGrowingTextView
With this you can manage animation and maximum height of text view.
Related
I have this problem, i have set in the Storyboard 2 UIButton and after touching one of them they must move, so i implemented this using animation, and works fine. But after the completion of the animation i'm performing a modal segue, the problem is that while modal segue is starting, raising the new UIView from the bottom, the 2 UIButton return to their previous place, where i set them in the Storyboard, and it's orribile to see. How could i solve this?
Here's the code i'm using
#property (strong, nonatomic) IBOutlet UIButton *maleBtn;
#property (strong, nonatomic) IBOutlet UIButton *femaleBtn;
- (void)viewDidLoad
{
[super viewDidLoad];
[_maleBtn addTarget:self
action:#selector(chooseGender:)
forControlEvents:UIControlEventTouchUpInside];
[_femaleBtn addTarget:self
action:#selector(chooseGender:)
forControlEvents:UIControlEventTouchUpInside];
}
- (IBAction)chooseGender:(id)sender
{
CGRect maleFrame;
CGRect femaleFrame;
if (sender == _maleBtn) {
maleFrame = self.maleBtn.frame;
maleFrame.origin.x = (self.view.bounds.size.width/2) - 15;
femaleFrame = self.femaleBtn.frame;
femaleFrame.origin.x = self.view.bounds.size.height;
}
else {
maleFrame = self.maleBtn.frame;
maleFrame.origin.x = -self.view.bounds.size.height;
femaleFrame = self.femaleBtn.frame;
femaleFrame.origin.x = (self.view.bounds.size.width/2) - 15;
}
[UIView animateWithDuration:0.5
delay:0.5
options:UIViewAnimationCurveEaseOut
animations:^{
_maleBtn.frame = maleFrame;
_femaleBtn.frame = femaleFrame;
}
completion:^(BOOL finished){
if(sender == _maleBtn)
[self performSegueWithIdentifier:#"toMale" sender:self];
else
[self performSegueWithIdentifier:#"toFemale" sender:self];
}];
}
At the top you make a bool or add a property in your .h if you like. But I would just use a local variable for this.
BOOL didAnimate;
In viewDidLoad you set it
- (void)viewDidLoad
{
[super viewDidLoad];
//Set the bool value here to no;
didAnimate = NO;
[_maleBtn addTarget:self
action:#selector(chooseGender:)
forControlEvents:UIControlEventTouchUpInside];
[_femaleBtn addTarget:self
action:#selector(chooseGender:)
forControlEvents:UIControlEventTouchUpInside];
}
Use it here, and I also took the liberty to rewrite your code a bit.
- (IBAction)chooseGender:(id)sender
{
CGRect maleFrame;
CGRect femaleFrame;
if (sender == _maleBtn) {
maleFrame = self.maleBtn.frame;
maleFrame.origin.x = (self.view.bounds.size.width/2) - 15;
femaleFrame = self.femaleBtn.frame;
femaleFrame.origin.x = self.view.bounds.size.height;
}
else {
maleFrame = self.maleBtn.frame;
maleFrame.origin.x = -self.view.bounds.size.height;
femaleFrame = self.femaleBtn.frame;
femaleFrame.origin.x = (self.view.bounds.size.width/2) - 15;
}
[UIView animateWithDuration:0.5
delay:0.5
options:UIViewAnimationCurveEaseOut
animations:^{
_maleBtn.frame = maleFrame;
_femaleBtn.frame = femaleFrame;
}
completion:^(BOOL finished){
didAnimate = YES;
}];
if(sender == _maleBtn && didAnimate){
[self performSegueWithIdentifier:#"toMale" sender:self];
}
else if((sender == _femaleBtn && didAnimate)){
[self performSegueWithIdentifier:#"toFemale" sender:self];
}
else{
NSLog(#"failure to transition");
}
}
I have not tested this and there might be spelling errors and such. So copy/paste might not work.
I'm implementing an autogrowing UITextView. I'm aiming for a similar behaviour of the message box in Whatsapp, which autogrows when your text has more than 1 line.
I'm using the approach described below which stores the height constraint in a UITextView subclass and modifies it when the text changes.
My solution animates correctly when I press the enter key inside the TextView, but it doesn't work when my typing goes over the end of the line. In this case it just changes size instantly.
Performing the animation on the delegate's - (void)textViewDidChange:(UITextView *)textView method produces the same result.
How can I correctly animate the TextView height using the auto layout system?
I'm implementing it like this:
#interface OEAutoGrowingTextView ()
#property (strong, nonatomic) NSLayoutConstraint *heightConstraint;
#end
#implementation OEAutoGrowingTextView
- (id)initWithFrame:(CGRect)frame
{
if ( !(self = [super initWithFrame:frame]) )
{
return nil;
}
[self commonInit];
return self;
}
- (void)awakeFromNib
{
[self commonInit];
}
- (void)commonInit
{
// If we are using auto layouts, than get a handler to the height constraint.
for (NSLayoutConstraint *constraint in self.constraints)
{
if (constraint.firstAttribute == NSLayoutAttributeHeight)
{
self.heightConstraint = constraint;
break;
}
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(textDidChange:) name:UITextViewTextDidChangeNotification object:self];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)textDidChange:(NSNotification *)notification
{
self.heightConstraint.constant = self.contentSize.height;
[UIView animateWithDuration:1.0f animations:^
{
[self layoutIfNeeded];
}];
}
#end
Note: doing the following doesn't help.
- (void)textDidChange:(NSNotification *)notification
{
self.heightConstraint.constant = self.contentSize.height;
[UIView animateWithDuration:1.0f animations:^
{
[self layoutIfNeeded];
for (UIView *view in self.subviews)
{
[view layoutIfNeeded];
}
}];
}
Further update: This seems to be a bug in iOS 7.x, I think it's fixed on iOS 8.0.
I tried wrapping the heightConstraint change in a UIView animation block and that didn't work
That isn't how you animate a constraint change. You do it by changing the constraint and then animating the act of layout itself, like this:
// change the text view constraint here
[UIView animateWithDuration:duration animations:^{
[self.textView layoutIfNeeded];
}];
Ok, the issue is that as of ios7, .contentSize isn't correct for UITextViews. I have this functionality, and you need to compute the contentSize yourself. I added a category method to UITextView, -contentHeight, and use that instead to compute the contentSize.
See these two links.
UITextView Content Size
SO on the same question
Here is the code that fixes it:
#implementation UITextView (Sizing)
- (CGFloat)contentHeight {
if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1) {
// This is the code for iOS 7. contentSize no longer returns the correct value, so
// we have to calculate it.
//
// This is partly borrowed from HPGrowingTextView, but I've replaced the
// magic fudge factors with the calculated values (having worked out where
// they came from)
CGRect frame = self.bounds;
// Take account of the padding added around the text.
UIEdgeInsets textContainerInsets = self.textContainerInset;
UIEdgeInsets contentInsets = self.contentInset;
CGFloat leftRightPadding = textContainerInsets.left + textContainerInsets.right + self.textContainer.lineFragmentPadding * 2;
leftRightPadding += contentInsets.left + contentInsets.right;
CGFloat topBottomPadding = textContainerInsets.top + textContainerInsets.bottom + contentInsets.top + contentInsets.bottom;
frame.size.width -= leftRightPadding;
frame.size.height -= topBottomPadding;
NSString* textToMeasure = self.text;
if(![textToMeasure isNotEmpty])
textToMeasure = #"-";
if ([textToMeasure hasSuffix:#"\n"]) {
textToMeasure = [NSString stringWithFormat:#"%#-", self.text];
}
// NSString class method: boundingRectWithSize:options:attributes:context is
// available only on ios7.0 sdk.
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping];
NSDictionary* attributes = #{NSFontAttributeName : self.font,
NSParagraphStyleAttributeName : paragraphStyle};
CGRect size = [textToMeasure boundingRectWithSize:CGSizeMake(CGRectGetWidth(frame), MAXFLOAT)
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
attributes:attributes
context:nil];
CGFloat measuredHeight = ceilf(CGRectGetHeight(size) + topBottomPadding);
return measuredHeight;
} else {
return self.contentSize.height;
}
}
#end
Instead of contentSize, use this to compute the content height. You also don't need the animate at all - mine just computes and that is smooth enough, so you should make sure you really need the animation.
I've read multiple solutions here on SO, but they all don;t seem to work in my case.
I've got a class NotificationBar:
.h file:
#interface NotificationBar : UIView <UIGestureRecognizerDelegate>
#property (strong, nonatomic) UILabel *messageLabel;
- (id)initWithFrame:(CGRect)frame message:(NSString *)message;
- (void) show:(int)duration;
#end
.m file:
#import "NotificationBar.h"
#implementation NotificationBar
#synthesize messageLabel;
- (id)initWithFrame:(CGRect)frame message:(NSString *)message
{
self = [super initWithFrame:frame];
if (self)
{
self.alpha = 0;
self.backgroundColor = [UIColor cabmanBlue];
self.userInteractionEnabled = YES;
self.messageLabel = [[UILabel alloc] initWithFrame:CGRectMake(self.bounds.origin.x + 5, 0 , self.bounds.size.width - 10, self.bounds.size.height)];
[self.messageLabel setBackgroundColor:[UIColor clearColor]];
[self.messageLabel setTextColor:[UIColor whiteColor]];
[self.messageLabel setFont:[UIFont boldSystemFontOfSize:14]];
[self.messageLabel setNumberOfLines:2];
self.messageLabel.text = message;
[self addSubview:messageLabel];
UITapGestureRecognizer *tapRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(dismiss:)];
tapRec.delegate = self;
tapRec.cancelsTouchesInView = NO;
[self addGestureRecognizer:tapRec];
}
return self;
}
- (void) show:(int)duration
{
[[[[[UIApplication sharedApplication] keyWindow] subviews] lastObject] addSubview:self];
[UIView animateWithDuration:0.5 animations:^{ self.alpha = 1; } completion:^(BOOL finished) {
[UIView animateWithDuration:0.5 delay:duration options:UIViewAnimationOptionCurveLinear animations:^{ self.alpha = 0;} completion:^(BOOL finished)
{
if(finished) [self removeFromSuperview];
}];
}];
}
- (void) dismiss:(UITapGestureRecognizer *)gesture
{
[self.layer removeAllAnimations];
[UIView animateWithDuration:0.5 animations:^{ self.alpha = 0; } completion:^(BOOL finished) {
if(finished)
{
[self removeFromSuperview];
}
}];
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
#end
And I use the class in the following way:
- (void) displayNotificationMessage:(NSString *)message withSound:(BOOL) withSound
{
UIView *topView = [self.window.subviews lastObject];
[[[NotificationBar alloc] initWithFrame:CGRectMake(topView.bounds.origin.x,
64,
topView.bounds.size.width,
40)
message:message] show:10];
if(withSound) AudioServicesPlaySystemSound(1007);
}
The view is always presented and shown. The dismiss function isn't triggered and it seems it doesn't respond to anything. displayNotificationMessage is placed in AppDelegate. displayNotificationMessage is some times used when a viewcontroller with a mapview is displayed or in a UITableViewController. You could say it has to work the same way as UIAlertView: always presented no matter in which screen the user is.
Does anyone see an error or something?
Thank in advance!
Your UILabel takes over almost your entire view bounds:
self.messageLabel = [[UILabel alloc] initWithFrame:CGRectMake(self.bounds.origin.x + 5, 0 , self.bounds.size.width - 10, self.bounds.size.height)];
You may try reducing it's size and then see if the gesture works in the other locations or you can try adding the gesture to the label
[self.messageLabel addGestureRecognizer:singleTap];
I just tried your code on a custom UIView and it works just fine. Having the UILabel as a subview has no effect, it responds well. To me it looks like you probably have either another UIView put over this one (which makes this one buried and therefore unresponsive) or you have another Tap Gesture registered in your superview.
Since iOS 7, a UITextView does not scroll automatically to the cursor as the user types text that flows to a new line. This issue is well documented on SO and elsewhere. For me, the issue is still present in iOS 7.1. What am I doing wrong?
I installed Xcode 5.1 and targeted iOS 7.1. I'm using Auto Layout.
Here's how I position the text view's content above the keyboard:
- (void)keyboardUp:(NSNotification *)notification
{
NSDictionary *info = [notification userInfo];
CGRect keyboardRect = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
keyboardRect = [self.view convertRect:keyboardRect fromView:nil];
UIEdgeInsets contentInset = self.textView.contentInset;
contentInset.bottom = keyboardRect.size.height;
self.textView.contentInset = contentInset;
}
What I have tried: I have tried many of the solutions posted to SO on this issue as it pertains to iOS 7. All of the solutions that I have tried do not seem to hold up well for text views displaying an attributed string. In the following three steps, I outline how the most up-voted answer on SO (https://stackoverflow.com/a/19277383/1239263) responds to the user tapping the return key for the first time.
(1.) The text view became the first responder in viewDidLoad. Scroll to the bottom of the text view where the cursor is located.
(2.) Before typing a single character, tap the return key on the keyboard. The caret disappears out of sight.
(3.) Tapping the return key again, however, seems to normalize the situation. (Note: deleting the latter new line, however, makes the caret disappear once again).
Improved solution's code for UITextView descendant class:
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
#define is_iOS7 SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0")
#define is_iOS8 SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"8.0")
#implementation MyTextView {
BOOL settingText;
}
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleTextViewDidChangeNotification:) name:UITextViewTextDidChangeNotification object:self];
}
return self;
}
- (void)scrollToCaretInTextView:(UITextView *)textView animated:(BOOL)animated {
CGRect rect = [textView caretRectForPosition:textView.selectedTextRange.end];
rect.size.height += textView.textContainerInset.bottom;
[textView scrollRectToVisible:rect animated:animated];
}
- (void)handleTextViewDidChangeNotification:(NSNotification *)notification {
if (notification.object == self && is_iOS7 && !is_iOS8 && !settingText) {
UITextView *textView = self;
if ([textView.text hasSuffix:#"\n"]) {
[CATransaction setCompletionBlock:^{
[self scrollToCaretInTextView:textView animated:NO];
}];
} else {
[self scrollToCaretInTextView:textView animated:NO];
}
}
}
- (void)setText:(NSString *)text {
settingText = YES;
[super setText:text];
settingText = NO;
}
Note it doesn't work when Down key is pressed on Bluetooth keyboard.
A robust solution should hold up in the following situations:
(1.) a text view displaying an attributed string
(2.) a new line created by tapping the return key on the keyboard
(3.) a new line created by typing text that overflows to the next line
(4.) copy and paste text
(5.) a new line created by tapping the return key for the first time (see the 3 steps in the OP)
(6.) device rotation
(7.) some case I can't think of that you will...
To satisfy these requirements in iOS 7.1, it seems as though it's still necessary to manually scroll to the caret.
It's common to see solutions that manually scroll to the caret when the text view delegate method textViewDidChange: is called. However, I found that this technique did not satisfy situation #5 above. Even a call to layoutIfNeeded before scrolling to the caret didn't help. Instead, I had to scroll to the caret inside a CATransaction completion block:
// this seems to satisfy all of the requirements listed aboveāif you are targeting iOS 7.1
- (void)textViewDidChange:(UITextView *)textView
{
if ([textView.text hasSuffix:#"\n"]) {
[CATransaction setCompletionBlock:^{
[self scrollToCaretInTextView:textView animated:NO];
}];
} else {
[self scrollToCaretInTextView:textView animated:NO];
}
}
Why does this work? I have no idea. You'll have to ask an Apple engineer.
For completeness, here's all of the code related to my solution:
#import "ViewController.h"
#interface ViewController () <UITextViewDelegate>
#property (weak, nonatomic) IBOutlet UITextView *textView; // full-screen
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *string = #"All work and no play makes Jack a dull boy.\n\nAll work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy.";
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:#{NSFontAttributeName: [UIFont fontWithName:#"Verdana" size:30.0]}];
self.textView.attributedText = attrString;
self.textView.delegate = self;
self.textView.backgroundColor = [UIColor yellowColor];
[self.textView becomeFirstResponder];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardIsUp:) name:UIKeyboardDidShowNotification object:nil];
}
// helper method
- (void)scrollToCaretInTextView:(UITextView *)textView animated:(BOOL)animated
{
CGRect rect = [textView caretRectForPosition:textView.selectedTextRange.end];
rect.size.height += textView.textContainerInset.bottom;
[textView scrollRectToVisible:rect animated:animated];
}
- (void)keyboardIsUp:(NSNotification *)notification
{
NSDictionary *info = [notification userInfo];
CGRect keyboardRect = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
keyboardRect = [self.view convertRect:keyboardRect fromView:nil];
UIEdgeInsets inset = self.textView.contentInset;
inset.bottom = keyboardRect.size.height;
self.textView.contentInset = inset;
self.textView.scrollIndicatorInsets = inset;
[self scrollToCaretInTextView:self.textView animated:YES];
}
- (void)textViewDidChange:(UITextView *)textView
{
if ([textView.text hasSuffix:#"\n"]) {
[CATransaction setCompletionBlock:^{
[self scrollToCaretInTextView:textView animated:NO];
}];
} else {
[self scrollToCaretInTextView:textView animated:NO];
}
}
#end
If you find a situation where this doesn't work, please let me know.
I solved it by getting the actual position of the caret and adjusting to it, here's my method:
- (void) alignTextView:(UITextView *)textView withAnimation:(BOOL)shouldAnimate {
// where the blinky caret is
CGRect caretRect = [textView caretRectForPosition:textView.selectedTextRange.start];
CGFloat offscreen = caretRect.origin.y + caretRect.size.height - (textView.contentOffset.y + textView.bounds.size.height - textView.contentInset.bottom - textView.contentInset.top);
CGPoint offsetP = textView.contentOffset;
offsetP.y += offscreen + 3; // 3 px -- margin puts caret 3 px above bottom
if (offsetP.y >= 0) {
if (shouldAnimate) {
[UIView animateWithDuration:0.2 animations:^{
[textView setContentOffset:offsetP];
}];
}
else {
[textView setContentOffset:offsetP];
}
}
}
If you only need to orient after the user presses return / enter, try:
- (void) textViewDidChange:(UITextView *)textView {
if ([textView.text hasSuffix:#"\n"]) {
[self alignTextView:textView withAnimation:NO];
}
}
Let me know if it works for you!
I can't find original source but it works on iOS7.1
- (void)textViewDidChangeSelection:(UITextView *)textView
{
if ([textView.text characterAtIndex:textView.text.length-1] != ' ') {
textView.text = [textView.text stringByAppendingString:#" "];
}
NSRange range0 = textView.selectedRange;
NSRange range = range0;
if (range0.location == textView.text.length) {
range = NSMakeRange(range0.location - 1, range0.length);
} else if (range0.length > 0 &&
range0.location + range0.length == textView.text.length) {
range = NSMakeRange(range0.location, range0.length - 1);
}
if (!NSEqualRanges(range, range0)) {
textView.selectedRange = range;
}
}
Some one have made a subclass that solves all scrolling0related issues in UITextView. The implementation couldn't be easier - switch UITextView with the subclass PSPDFTextView.
A post about it, showing what is fixed (With nice Gif animations) is here: Fixing UITextView on iOS 7
The git is here: PSPDFTextView
I've been strunggling with an issue on my project :
I've a mapView and I have to show the annotation presented below (The small (+) is my button)
I've subclassed MKAnnotationView and I have CustomAnnotationView.h like this :
#interface CustomAnnotationView : MKAnnotationView
#property (strong, nonatomic) UIImageView *calloutView;
#property (strong, nonatomic) UIButton *pinButton;
#property (strong, nonatomic) UIView *annView;
- (void)setSelected:(BOOL)selected animated:(BOOL)animated;
- (void)animateCalloutAppearance;
My CustomAnnotationView.m
- (void)setSelected:(BOOL)selected animated:(BOOL)animated{
[super setSelected:selected animated:animated];
if(selected)
{
//building my custom animation
annView = [[ UIView alloc ] initWithFrame:(CGRectMake(0, 0, 30, 30)) ];
annView.frame =CGRectMake(0, 0, 200, 50);
calloutView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:#"customcallout.png"]];
[calloutView setFrame:CGRectMake(-25, 50, 0, 0)];
[calloutView sizeToFit];
[self animateCalloutAppearance];
//I tried to add a button here but I could't do it like it should be
[annView addSubview:calloutView];
[self addSubview:annView];
}
else
{
//Remove your custom view...
[annView removeFromSuperview];
}
}
- (void)didAddSubview:(UIView *)subview{
if ([[[subview class] description] isEqualToString:#"UICalloutView"]) {
for (UIView *subsubView in subview.subviews) {
if ([subsubView class] == [UIImageView class]) {
UIImageView *imageView = ((UIImageView *)subsubView);
[imageView removeFromSuperview];
}else if ([subsubView class] == [UILabel class]) {
UILabel *labelView = ((UILabel *)subsubView);
[labelView removeFromSuperview];
}
}
}
}
- (void)animateCalloutAppearance {
CGFloat scale = 0.001f;
calloutView.transform = CGAffineTransformMake(scale, 0.0f, 0.0f, scale, 0, -50);
[UIView animateWithDuration:0.15 delay:0 options:UIViewAnimationCurveEaseOut animations:^{
CGFloat scale = 1.1f;
calloutView.transform = CGAffineTransformMake(scale, 0.0f, 0.0f, scale, 0, 2);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationCurveEaseInOut animations:^{
CGFloat scale = 0.95;
calloutView.transform = CGAffineTransformMake(scale, 0.0f, 0.0f, scale, 0, -2);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.075 delay:0 options:UIViewAnimationCurveEaseInOut animations:^{
CGFloat scale = 1.0;
calloutView.transform = CGAffineTransformMake(scale, 0.0f, 0.0f, scale, 0, 0);
} completion:nil];
}];
}];
}
#end
With this I'm able to show my custom annotation on the map but I can't figure out how to place a button on it and this button should of course be able to respond to clicks like the callback calloutAccessoryControlTapped:
Please anyone with a working example code or idea.
Thank you.
I solved this using - (UIView*)hitTest in my CustmAnnotationView.m class. I have a separate nib for the callout view and use hittest to determine whether the UILabel named directionsButton (linked from the callout nib) was clicked:
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
if (selected) {
calloutVisible = YES;
hitTestBuffer = NO;
self.calloutView = [[[NSBundle mainBundle] loadNibNamed:#"CalloutView" owner:self options:nil] objectAtIndex:0];
[directionsButton setFont:[UIFont fontWithName:#"DIN-Medium" size:12.0f]];
[calloutView setFrame:CGRectMake(calloutView.frame.origin.x, calloutView.frame.origin.y, 280, calloutView.frame.size.height)];
[calloutView setAlpha:0.0f];
[self addSubview:calloutView];
[self sendSubviewToBack:calloutView];
[UIView animateWithDuration:0.3f delay:0.0f options:0 animations:^{
[calloutView setAlpha:1.0f];
} completion:^(BOOL finished) {
}];
[super setSelected:selected animated:animated];
} else {
calloutVisible = NO;
CGRect newFrame = self.frame;
newFrame.size.width = 52;
self.frame = newFrame;
[UIView animateWithDuration:0.3f delay:0.0f options:0 animations:^{
[calloutView setAlpha:0.0f];
} completion:^(BOOL finished) {
[calloutView removeFromSuperview];
}];
}
}
- (void)directionsPressed
{
if ([parent respondsToSelector:#selector(directionsPressed:)])
[parent performSelector:#selector(directionsPressed:) withObject:self.stockist];
}
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (calloutVisible) {
CGPoint hitPoint = [calloutView convertPoint:point toView:directionsButton];
if ([calloutView pointInside:hitPoint withEvent:event]) {
if (!hitTestBuffer) {
[self directionsPressed];
hitTestBuffer = YES;
}
return [directionsButton hitTest:point withEvent:event];
} else {
hitTestBuffer = NO;
}
}
return [super hitTest:point withEvent:event];
}
In my code parent is my current VC. This is probably not the neatest way but it worked at the time.
The callout looks like this:
A couple of booleans calloutVisible and hitTestBuffer make sure the button is only clickable once and only when visible.
To try and resolve your issue, I played a bit with Apple's WeatherMap example, and came up with a working solution to your problem.
I added your code to the WeatherAnnotationView.m file (minus the animation code), and on top of that added the code to display the button, and respond to touch events:
-(void)annotationButtonClicked{
NSLog(#"** button clicked");
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated{
if(selected)
{
//building my custom animation
annView = [[ UIView alloc ] initWithFrame:(CGRectMake(0, 0, 30, 30)) ];
annView.frame =CGRectMake(0, 0, 200, 50);
calloutView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:#"cloudy.png"]];
[calloutView setFrame:CGRectMake(-25, 50, 0, 0)];
[calloutView sizeToFit];
[annView addSubview:calloutView];
/* BUTTON CODE */
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0,0,20,20)];
button.backgroundColor = [UIColor orangeColor];
[button addTarget:self action:#selector(annotationButtonClicked) forControlEvents:UIControlEventTouchUpInside];
[annView addSubview:button];
/* END Of BUTTON CODE */
[self addSubview:annView];
}
else
{
//Remove your custom view...
[annView removeFromSuperview];
}
}
You haven't posted HOW you tried to add a button to the annView, so it is hard for me to understand what was not working with your approach. A common approach for these kind of problems is to remove one piece of code at a time and try to run your app. For starters, try to remove the animation code. Then, try to remove calloutView, and remain only with annView and the button, until you have code that works with a button. After that, add back the pieces you removed earlier (animation, calloutView) one at a time and see which one is causing your button to stop function.
I hope this helps somehow.