mirror of
https://github.com/zhigang1992/MessagesTableViewController.git
synced 2026-04-02 22:40:05 +08:00
367 lines
14 KiB
Objective-C
367 lines
14 KiB
Objective-C
//
|
|
// JSMessagesViewController.m
|
|
//
|
|
// Created by Jesse Squires on 2/12/13.
|
|
// Copyright (c) 2013 Hexed Bits. All rights reserved.
|
|
//
|
|
// http://www.hexedbits.com
|
|
//
|
|
//
|
|
// Largely based on work by Sam Soffes
|
|
// https://github.com/soffes
|
|
//
|
|
// SSMessagesViewController
|
|
// https://github.com/soffes/ssmessagesviewcontroller
|
|
//
|
|
//
|
|
// The MIT License
|
|
// Copyright (c) 2013 Jesse Squires
|
|
//
|
|
// 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.
|
|
//
|
|
|
|
#import "JSMessagesViewController.h"
|
|
#import "NSString+JSMessagesView.h"
|
|
#import "UIView+AnimationOptionsForCurve.h"
|
|
#import "UIColor+JSMessagesView.h"
|
|
|
|
#define INPUT_HEIGHT 40.0f
|
|
|
|
@interface JSMessagesViewController ()
|
|
|
|
- (void)setup;
|
|
|
|
@end
|
|
|
|
|
|
|
|
@implementation JSMessagesViewController
|
|
|
|
#pragma mark - Initialization
|
|
- (void)setup
|
|
{
|
|
if([self.view isKindOfClass:[UIScrollView class]]) {
|
|
// fix for ipad modal form presentations
|
|
((UIScrollView *)self.view).scrollEnabled = NO;
|
|
}
|
|
|
|
CGSize size = self.view.frame.size;
|
|
|
|
CGRect tableFrame = CGRectMake(0.0f, 0.0f, size.width, size.height - INPUT_HEIGHT);
|
|
self.tableView = [[UITableView alloc] initWithFrame:tableFrame style:UITableViewStylePlain];
|
|
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
self.tableView.dataSource = self;
|
|
self.tableView.delegate = self;
|
|
[self.view addSubview:self.tableView];
|
|
|
|
[self setBackgroundColor:[UIColor messagesBackgroundColor]];
|
|
|
|
CGRect inputFrame = CGRectMake(0.0f, size.height - INPUT_HEIGHT, size.width, INPUT_HEIGHT);
|
|
self.inputView = [[JSMessageInputView alloc] initWithFrame:inputFrame delegate:self];
|
|
|
|
UIButton *sendButton = [self sendButton];
|
|
sendButton.enabled = NO;
|
|
sendButton.frame = CGRectMake(self.inputView.frame.size.width - 65.0f, 8.0f, 59.0f, 26.0f);
|
|
[sendButton addTarget:self
|
|
action:@selector(sendPressed:)
|
|
forControlEvents:UIControlEventTouchUpInside];
|
|
[self.inputView setSendButton:sendButton];
|
|
[self.view addSubview:self.inputView];
|
|
|
|
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
|
|
swipe.direction = UISwipeGestureRecognizerDirectionDown;
|
|
swipe.numberOfTouchesRequired = 1;
|
|
[self.inputView addGestureRecognizer:swipe];
|
|
}
|
|
|
|
- (UIButton *)sendButton
|
|
{
|
|
return [UIButton defaultSendButton];
|
|
}
|
|
|
|
#pragma mark - View lifecycle
|
|
- (void)viewDidLoad
|
|
{
|
|
[super viewDidLoad];
|
|
[self setup];
|
|
}
|
|
|
|
- (void)viewWillAppear:(BOOL)animated
|
|
{
|
|
[self scrollToBottomAnimated:NO];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(handleWillShowKeyboard:)
|
|
name:UIKeyboardWillShowNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(handleWillHideKeyboard:)
|
|
name:UIKeyboardWillHideNotification
|
|
object:nil];
|
|
}
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
|
|
}
|
|
|
|
- (void)didReceiveMemoryWarning
|
|
{
|
|
[super didReceiveMemoryWarning];
|
|
NSLog(@"*** %@: didReceiveMemoryWarning ***", self.class);
|
|
}
|
|
|
|
#pragma mark - View rotation
|
|
- (BOOL)shouldAutorotate
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (NSUInteger)supportedInterfaceOrientations
|
|
{
|
|
return UIInterfaceOrientationMaskPortrait;
|
|
}
|
|
|
|
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
|
|
{
|
|
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
|
|
[self.tableView reloadData];
|
|
[self.tableView setNeedsLayout];
|
|
}
|
|
|
|
#pragma mark - Actions
|
|
- (void)sendPressed:(UIButton *)sender
|
|
{
|
|
[self.delegate sendPressed:sender
|
|
withText:[self.inputView.textView.text trimWhitespace]];
|
|
}
|
|
|
|
- (void)handleSwipe:(UIGestureRecognizer *)guestureRecognizer
|
|
{
|
|
[self.inputView.textView resignFirstResponder];
|
|
}
|
|
|
|
#pragma mark - Table view data source
|
|
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
JSBubbleMessageStyle style = [self.delegate messageStyleForRowAtIndexPath:indexPath];
|
|
BOOL hasTimestamp = [self shouldHaveTimestampForRowAtIndexPath:indexPath];
|
|
BOOL hasAvatar = [self shouldHavePhotoForRowAtIndexPath:indexPath];
|
|
|
|
NSString *CellID = [NSString stringWithFormat:@"MessageCell_%d_%d", style, hasTimestamp];
|
|
JSBubbleMessageCell *cell = (JSBubbleMessageCell *)[tableView dequeueReusableCellWithIdentifier:CellID];
|
|
|
|
if(!cell) {
|
|
cell = [[JSBubbleMessageCell alloc] initWithBubbleStyle:style
|
|
hasTimestamp:hasTimestamp
|
|
hasAvatar:hasAvatar
|
|
reuseIdentifier:CellID];
|
|
}
|
|
|
|
if(hasTimestamp)
|
|
[cell setTimestamp:[self.dataSource timestampForRowAtIndexPath:indexPath]];
|
|
|
|
if (hasAvatar) {
|
|
[self.dataSource goCrazyWithYourAvatarImageView:cell.photoView forRowAtIndexPath:indexPath];
|
|
}
|
|
|
|
[cell setMessage:[self.dataSource textForRowAtIndexPath:indexPath]];
|
|
[cell setBackgroundColor:tableView.backgroundColor];
|
|
|
|
return cell;
|
|
}
|
|
|
|
#pragma mark - Table view delegate
|
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
CGFloat dateHeight = [self shouldHaveTimestampForRowAtIndexPath:indexPath] ? DATE_LABEL_HEIGHT : 0.0f;
|
|
CGFloat avatarCellHeight = [self shouldHavePhotoForRowAtIndexPath:indexPath] ? (PHOTO_SIZE.height + PHOTO_EDGE_INSET.top + PHOTO_EDGE_INSET.bottom) : 0.0f;
|
|
return MAX(avatarCellHeight, [JSBubbleView cellHeightForText:[self.dataSource textForRowAtIndexPath:indexPath]] + dateHeight);
|
|
}
|
|
|
|
#pragma mark - Messages view controller
|
|
- (BOOL)shouldHaveTimestampForRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
switch ([self.delegate timestampPolicyForMessagesView]) {
|
|
case JSMessagesViewTimestampPolicyAll:
|
|
return YES;
|
|
break;
|
|
case JSMessagesViewTimestampPolicyAlternating:
|
|
return indexPath.row % 2 == 0;
|
|
break;
|
|
case JSMessagesViewTimestampPolicyEveryThree:
|
|
return indexPath.row % 3 == 0;
|
|
break;
|
|
case JSMessagesViewTimestampPolicyEveryFive:
|
|
return indexPath.row % 5 == 0;
|
|
break;
|
|
case JSMessagesViewTimestampPolicyCustom:
|
|
return NO;
|
|
break;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)shouldHavePhotoForRowAtIndexPath:(NSIndexPath *)indexPath{
|
|
if ([self.delegate respondsToSelector:@selector(hasPhotoForRowAtIndexPath:)]) {
|
|
return [self.delegate hasPhotoForRowAtIndexPath:indexPath];
|
|
} else {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
- (void)finishSend
|
|
{
|
|
[self.inputView.textView setText:nil];
|
|
[self textViewDidChange:self.inputView.textView];
|
|
[self.tableView reloadData];
|
|
[self scrollToBottomAnimated:YES];
|
|
}
|
|
|
|
- (void)setBackgroundColor:(UIColor *)color
|
|
{
|
|
self.view.backgroundColor = color;
|
|
self.tableView.backgroundColor = color;
|
|
self.tableView.separatorColor = color;
|
|
}
|
|
|
|
- (void)scrollToBottomAnimated:(BOOL)animated
|
|
{
|
|
NSInteger rows = [self.tableView numberOfRowsInSection:0];
|
|
|
|
if(rows > 0) {
|
|
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:rows - 1 inSection:0]
|
|
atScrollPosition:UITableViewScrollPositionBottom
|
|
animated:animated];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Text view delegate
|
|
- (void)textViewDidBeginEditing:(UITextView *)textView
|
|
{
|
|
[textView becomeFirstResponder];
|
|
|
|
if(!self.previousTextViewContentHeight)
|
|
self.previousTextViewContentHeight = textView.contentSize.height;
|
|
|
|
[self scrollToBottomAnimated:YES];
|
|
}
|
|
|
|
- (void)textViewDidEndEditing:(UITextView *)textView
|
|
{
|
|
[textView resignFirstResponder];
|
|
}
|
|
|
|
- (void)textViewDidChange:(UITextView *)textView
|
|
{
|
|
CGFloat maxHeight = [JSMessageInputView maxHeight];
|
|
CGFloat textViewContentHeight = textView.contentSize.height;
|
|
BOOL isShrinking = textViewContentHeight < self.previousTextViewContentHeight;
|
|
CGFloat changeInHeight = textViewContentHeight - self.previousTextViewContentHeight;
|
|
|
|
changeInHeight = (textViewContentHeight + changeInHeight >= maxHeight) ? 0.0f : changeInHeight;
|
|
|
|
if(!isShrinking)
|
|
[self.inputView adjustTextViewHeightBy:changeInHeight];
|
|
|
|
if(changeInHeight != 0.0f) {
|
|
[UIView animateWithDuration:0.25f
|
|
animations:^{
|
|
UIEdgeInsets insets = UIEdgeInsetsMake(0.0f, 0.0f, self.tableView.contentInset.bottom + changeInHeight, 0.0f);
|
|
self.tableView.contentInset = insets;
|
|
self.tableView.scrollIndicatorInsets = insets;
|
|
|
|
[self scrollToBottomAnimated:NO];
|
|
|
|
CGRect inputViewFrame = self.inputView.frame;
|
|
self.inputView.frame = CGRectMake(0.0f,
|
|
inputViewFrame.origin.y - changeInHeight,
|
|
inputViewFrame.size.width,
|
|
inputViewFrame.size.height + changeInHeight);
|
|
}
|
|
completion:^(BOOL finished) {
|
|
if(isShrinking)
|
|
[self.inputView adjustTextViewHeightBy:changeInHeight];
|
|
}];
|
|
|
|
self.previousTextViewContentHeight = MIN(textViewContentHeight, maxHeight);
|
|
}
|
|
|
|
self.inputView.sendButton.enabled = ([textView.text trimWhitespace].length > 0);
|
|
}
|
|
|
|
#pragma mark - Keyboard notifications
|
|
- (void)handleWillShowKeyboard:(NSNotification *)notification
|
|
{
|
|
[self keyboardWillShowHide:notification];
|
|
}
|
|
|
|
- (void)handleWillHideKeyboard:(NSNotification *)notification
|
|
{
|
|
[self keyboardWillShowHide:notification];
|
|
}
|
|
|
|
- (void)keyboardWillShowHide:(NSNotification *)notification
|
|
{
|
|
CGRect keyboardRect = [[notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
|
UIViewAnimationCurve curve = [[notification.userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue];
|
|
double duration = [[notification.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
|
|
|
[UIView animateWithDuration:duration
|
|
delay:0.0f
|
|
options:[UIView animationOptionsForCurve:curve]
|
|
animations:^{
|
|
CGFloat keyboardY = [self.view convertRect:keyboardRect fromView:nil].origin.y;
|
|
|
|
CGRect inputViewFrame = self.inputView.frame;
|
|
CGFloat inputViewFrameY = keyboardY - inputViewFrame.size.height;
|
|
|
|
// for ipad modal form presentations
|
|
CGFloat messageViewFrameBottom = self.view.frame.size.height - INPUT_HEIGHT;
|
|
if(inputViewFrameY > messageViewFrameBottom)
|
|
inputViewFrameY = messageViewFrameBottom;
|
|
|
|
self.inputView.frame = CGRectMake(inputViewFrame.origin.x,
|
|
inputViewFrameY,
|
|
inputViewFrame.size.width,
|
|
inputViewFrame.size.height);
|
|
|
|
UIEdgeInsets insets = UIEdgeInsetsMake(0.0f,
|
|
0.0f,
|
|
self.view.frame.size.height - self.inputView.frame.origin.y - INPUT_HEIGHT,
|
|
0.0f);
|
|
|
|
self.tableView.contentInset = insets;
|
|
self.tableView.scrollIndicatorInsets = insets;
|
|
}
|
|
completion:^(BOOL finished) {
|
|
}];
|
|
}
|
|
|
|
@end |