1

I'm attempting to create a custom message cell that has an icon (on the right hand side for incoming and left hand side for outgoing)

I've subclassed JSQMessagesCollectionViewCellOutgoing and have the icon displayed where I want. great. What I haven't been able to do is have the Text View's width smaller to take into account the icon. I thought it was a simple autolayout change in my custom outgoing xib file, but when the app runs, the autolayout width is overridden.

How do I update (directly or indirectly) textViewMarginHorizontalSpaceConstraint within JSQMessagesCollectionViewCell from a subclass of either JSQMessagesCollectionViewCell or JSQMessagesCollectionViewCellOutgoing

adamf
  • 63
  • 1
  • 9

2 Answers2

1

You don't have to create any extension of JSQMessagesCollectionViewCell,

unless if you want to change the structure of the Cell say change the position of the sender/receiver avatar or the upper/lower label position.

I'm pretty new of JSQMessage but here's my solution:

If you just want a custom view of the main content part of the message, which is the bubble rounded corner part :), then

all you need is a custom JSQMediaItem class,

and you override the

- (UIView *)mediaView

method, then you get full control of the content view.

The trick part is, you have to get the height of your content view before

- (CGSize)mediaViewDisplaySize

this method is called by the JSQ framework. So the best way is dynamically calculate the height of the returned view just in the init method of your custom class:

- (instancetype)initWithImage:(UIImage *)image title:(NSString*)title message:(NSString*)msg

Here's my custom class under the require of showing an event message, a title, an image, and a message. Btw, only the message part height is dynamic so I just calculated this part using

[NSString boundingRectWithSize:]

EventMediaItem.h

#import <JSQMessagesViewController/JSQMediaItem.h>

NS_ASSUME_NONNULL_BEGIN

/**
 *  The `EventMediaItem` class is a concrete `JSQMediaItem` subclass that implements the `JSQMessageMediaData` protocol
 *  and represents a event media message. An initialized `EvnetMediaItem` object can be passed 
 *  to a `JSQMediaMessage` object during its initialization to construct a valid media message object.
 *  You may wish to subclass `EventMediaItem` to provide additional functionality or behavior.
 */
@interface EventMediaItem : JSQMediaItem <JSQMessageMediaData, NSCoding, NSCopying>

/**
 *  The image for the event media item. The default value is `nil`.
 */
@property (copy, nonatomic, nullable) UIImage *image;
/**
 *  The title for the event media item. The default value is `nil`.
 */
@property (copy, nonatomic, nullable) NSString *title;
/**
 *  The message for the event media item. The default value is `nil`.
 */
@property (copy, nonatomic, nullable) NSString *message;

/**
 *  Initializes and returns a event media item object having the given image.
 *
 *  @param image The image for the event media item. This value may be `nil`.
 *  @param title The title for the event media item. This value may be `nil`.
 *  @param message The message for the event media item. This value may be `nil`.
 *
 *  @return An initialized `EventMediaItem`.
 *
 *  @discussion If the image must be dowloaded from the network, 
 *  you may initialize a `EventMediaItem` object with a `nil` image. 
 *  Once the image has been retrieved, you can then set the image property.
 */

- (instancetype)initWithImage:(UIImage *)image title:(NSString*)title message:(NSString*)msg;

@end

NS_ASSUME_NONNULL_END

And EventMediaItem.m

#import "EventMediaItem.h"

#import "JSQMessagesMediaPlaceholderView.h"
#import "JSQMessagesMediaViewBubbleImageMasker.h"
#import "UIImage+JSQMessages.h"
#import "JSQMessagesBubbleImageFactory.h"

#import <MobileCoreServices/UTCoreTypes.h>

#define paddingStart 15.0f
#define indent 10.0f
#define spacing 7.0f

#define paddingTop 3.0f
#define heightTitle 40.0f
#define heightImage 100.0f
#define magicDelta 20.0f


@interface EventMediaItem (){
    CGFloat heightMsgView;
    NSDictionary *attrsDictionary;
}

@property (strong, nonatomic) UIView *rootView;
@property (strong, nonatomic) UIImageView *cachedImageView;
@property (strong, nonatomic) UILabel *titleView;
@property (strong, nonatomic) UITextView *messageView;

@end


@implementation EventMediaItem

#pragma mark - Initialization

- (instancetype)initWithImage:(UIImage *)image title:(NSString*)title message:(NSString*)msg
{
    self = [super init];
    if (self) {
        _image = [image copy];
        _cachedImageView = nil;
        _message = [msg copy];
        _messageView = nil;
        _title = [title copy];
        _titleView = nil;

        CGSize size;

        size = CGSizeMake(210.0, 500);

        if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
            size = CGSizeMake(315.0, 500);
        }

        UITextView *tmp = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height-(paddingTop+heightImage+heightTitle))];

        NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
        paragraphStyle.headIndent = indent;
        paragraphStyle.firstLineHeadIndent = indent;
        paragraphStyle.lineSpacing = spacing;
        UIFont *font = [UIFont fontWithName:@"HelveticaNeue" size:14.f];
        // save the attrs for future use
        attrsDictionary =
        @{ NSParagraphStyleAttributeName: paragraphStyle, NSFontAttributeName:font}; // <-- there are many more attrs, e.g NSFontAttributeName

        tmp.attributedText = [[NSAttributedString alloc] initWithString:_message attributes:attrsDictionary];


        // *** ADJUST VIEW SIZE ***
        CGRect containSize = [tmp.text boundingRectWithSize:CGSizeMake(size.width, 400) options:NSStringDrawingUsesLineFragmentOrigin attributes:attrsDictionary context:nil];
        heightMsgView = containSize.size.height;
        // *** ADJUST VIEW SIZE ***



    }
    return self;
}

- (void)clearCachedMediaViews
{
    [super clearCachedMediaViews];
    _cachedImageView = nil;
}

#pragma mark - Setters

- (void)setImage:(UIImage *)image
{
    _image = [image copy];
    _cachedImageView = nil;
}

- (void)setAppliesMediaViewMaskAsOutgoing:(BOOL)appliesMediaViewMaskAsOutgoing
{
    [super setAppliesMediaViewMaskAsOutgoing:appliesMediaViewMaskAsOutgoing];
    _cachedImageView = nil;
}

#pragma mark - JSQMessageMediaData protocol

- (UIView *)mediaView
{

    UIColor *bgc = [UIColor colorWithRed:243/255.0 green:243/255.0 blue:243/255.0 alpha:1];

    if (self.image == nil) {
        return nil;
    }

    if (self.cachedImageView == nil) {
        CGSize size = CGSizeMake(210.0, 400);
        self.rootView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, size.width, paddingTop+heightTitle+heightImage+heightMsgView+magicDelta)];
        self.rootView.backgroundColor = bgc;

        self.titleView = [[UILabel alloc] initWithFrame:CGRectMake(paddingStart, paddingTop, size.width-15, heightTitle)];
        self.titleView.font = [UIFont systemFontOfSize:16 weight:20];
        self.titleView.textColor = [UIColor darkGrayColor];
        self.titleView.numberOfLines = 2;
        self.titleView.text = _title;

        UIImageView *imageView = [[UIImageView alloc] initWithImage:self.image];
        imageView.frame = CGRectMake(0.0f, self.titleView.frame.size.height, size.width, heightImage);
        imageView.contentMode = UIViewContentModeScaleAspectFill;
        imageView.clipsToBounds = YES;
        self.cachedImageView = imageView;

        self.messageView = [[UITextView alloc] initWithFrame:CGRectMake(00.0f, self.titleView.frame.size.height+self.cachedImageView.frame.size.height, size.width, heightMsgView+magicDelta)];
        self.messageView.textColor = [UIColor darkGrayColor];
        self.messageView.font = [UIFont systemFontOfSize:15];
        self.messageView.backgroundColor = bgc;
        self.messageView.editable = NO;

        //---
        NSMutableAttributedString* attrString = [[NSMutableAttributedString alloc] initWithString:_message attributes:attrsDictionary];
        NSRange range = [self.message rangeOfString:NSLocalizedStringFromTable(@"EventListMore", @"Message", @"")];
        [attrString addAttribute:NSForegroundColorAttributeName value:[UIColor colorWithRed:108/255.0 green:210/255.0 blue:240/255.0 alpha:1] range:range]; //6cd2f0
        //---
        self.messageView.attributedText = attrString;


        //add the views to mediaView
        [self.rootView addSubview:self.cachedImageView];
        [self.rootView addSubview:self.messageView];
        [self.rootView addSubview:self.titleView];

        //apply bubble factory
        JSQMessagesBubbleImageFactory *factory = [[JSQMessagesBubbleImageFactory alloc] initWithBubbleImage:[UIImage jsq_bubbleRegularTaillessImage] capInsets:UIEdgeInsetsZero];
        JSQMessagesMediaViewBubbleImageMasker*masker = [[JSQMessagesMediaViewBubbleImageMasker alloc] initWithBubbleImageFactory:factory];
        [masker applyIncomingBubbleImageMaskToMediaView:self.rootView];

    }

    return self.rootView;
}

- (CGSize)mediaViewDisplaySize
{
    if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
        return CGSizeMake(315.0f, paddingTop+heightTitle+heightImage+heightMsgView);
    }

    return CGSizeMake(210.0f, paddingTop+heightTitle+heightImage+heightMsgView+magicDelta);
}

- (NSUInteger)mediaHash
{
    return self.hash;
}

- (NSString *)mediaDataType
{
    return (NSString *)kUTTypeMessage;
}

- (id)mediaData
{
    return UIImageJPEGRepresentation(self.image, 1);
}

#pragma mark - NSObject

- (NSUInteger)hash
{
    return super.hash ^ self.image.hash;
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"<%@: image=%@, appliesMediaViewMaskAsOutgoing=%@>",
            [self class], self.image, @(self.appliesMediaViewMaskAsOutgoing)];
}

#pragma mark - NSCoding

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        _image = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(image))];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [super encodeWithCoder:aCoder];
    [aCoder encodeObject:self.image forKey:NSStringFromSelector(@selector(image))];
}

#pragma mark - NSCopying

- (instancetype)copyWithZone:(NSZone *)zone
{
    EventMediaItem *copy = [[EventMediaItem allocWithZone:zone] initWithImage:self.image title:self.title message:self.message];
    copy.appliesMediaViewMaskAsOutgoing = self.appliesMediaViewMaskAsOutgoing;
    return copy;
}

@end

How to use it: First, register the cell nib(Swift project)

self.collectionView.registerNib(UINib(nibName: "VobMessagesMediaCellIncoming", bundle: nil), forCellWithReuseIdentifier: "VobMessagesMediaCellIncoming")

VobMessagesMediaCellIncoming is just a copy of any incomming message cell you are using right now, as my experience you have to copy a new one with different identifier rather than using the same nib for normal message and media message, or the JSQMessageCollectionView may confuse and showing abnormal message when reuse the cell.

Then, in the cellForItemAtIndexPath you would fork the logic to NOT set the textView of the cell, because the corresponding message is Media-type(EventMediaItem) and any setting of textView of the cell will cause assertion failure :

    override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = super.collectionView(collectionView, cellForItemAtIndexPath: indexPath) as! JSQMessagesCollectionViewCell
        let message = viewModel.messageBubbleModelView(indexPath.item)
        if !message.isMediaMessage {           
            let color = senderId == message.senderId ? UIColor.whiteColor() : AppColor.BodyTextColor
            cell.textView.textColor = color

            cell.textView.linkTextAttributes = [
                NSForegroundColorAttributeName: color,
                NSUnderlineStyleAttributeName: NSUnderlineStyle.StyleSingle.rawValue
            ]
        }

        //set position nickname
        cell.messageBubbleTopLabel.textAlignment = .Left
        let edgeInset = cell.messageBubbleTopLabel.textInsets
        cell.messageBubbleTopLabel.textInsets = UIEdgeInsetsMake(edgeInset.top, edgeInset.left - 20, edgeInset.bottom, edgeInset.right + 20)

        ...
    }

Btw, isMediaMessage property is from JSQMessage, if you init a JSQMessage and it will be set to ture; Finally, in your custom JSQMessage: (MessagePost is the message model of my own project)

class MessageBubbleViewModel: JSQMessage {
    let messagePost: MessagePost


    init(messagePost: MessagePost) {
        self.messagePost = messagePost
        if messagePost.isEvent {
            let eventMedia:EventMediaItem = EventMediaItem(image: UIImage(named: "some cool image")!, title: messagePost.title, message: messagePost.msg)

            super.init(senderId: String(messagePost.senderId),
                       senderDisplayName: messagePost.senderNickName,
                       date: messagePost.updatedAt,
                       media: eventMedia)
        }else{
            super.init(
                senderId: String(messagePost.senderId),
                senderDisplayName: messagePost.senderNickName,
                date: messagePost.updatedAt,
                text: messagePost.text)
        }
    }


    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

There seems to be a lot of other way to achieve this, you may refer to this link: https://github.com/jessesquires/JSQMessagesViewController/issues?utf8=%E2%9C%93&q=+%5Bcustom+cells%5D+in%3Atitle However, I cannot get any useful information from those threads. --#

teslac
  • 11
  • 1
0

I would suggest doing something like this

extension JSQMessagesCollectionViewCellOutgoing {
    public func messageContentSmaller() {
       self.messageBubbleContainerView?.setContentCompressionResistancePriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal)

    }
}

You may also need self.messageBubbleContainerView.setNeedsLayout

let me know if that helps.

Dan Leonard
  • 3,325
  • 1
  • 20
  • 32