Customization

A core design goal of this library is ease of customization so that both look-and-feel as well as behaviors can match your design goals.

By default, Atlas looks and behaves very much like iMessage. Without doing anything other than presenting the default views you’ll get:

  • A list of Conversations with the title set to the Participant names, the text of the last message, and an Avatar bubble of the Participant. This also includes the ability to swipe and delete the Conversation.
  • A Conversation view with grey incoming Messages on the left and blue outgoing Messages on the right, Sent/Delivered/Read receipts under the last message, a “3-dot” animated typing indicator, date and time stamps above each message group, and Avatar bubbles in group conversations
  • Message bubbles that handle rendering text, images, video, and location
  • A searchable Participants table view with their name and Avatar bubble

Note

Atlas is an open-source project and was designed to be customizable and flexible for a wide array of code bases and use cases. As such, not every configuration is guaranteed to work without issue. If you are integrating Atlas with a high level of customization, or extending it’s capabilities beyond our current build, you can submit a Pull Request to our Atlas iOS GitHub repository with your enhancements/bugfixes for us to review and merge.

Custom Appearance

Configuring Avatars

By default, Atlas behaves like iMessage when showing Avatars.

  • Avatars don’t appear in 1:1 conversations, only group conversations. To show Avatars in 1:1 conversations you must set the shouldDisplayAvatarItemForOneOtherParticipant property to YES.
conversationViewController.shouldDisplayAvatarItemForOneOtherParticipant = YES;
  • The authenticated user’s avatar will not be shown on the right side of the conversation. To show the current user’s avatar you must set the shouldDisplayAvatarItemForAuthenticatedUser property to YES.
conversationViewController.shouldDisplayAvatarItemForAuthenticatedUser = YES;

You can also show avatar images in the Conversation List.

  1. Set the displaysAvatarItem property to YES.
self.conversationListViewController.displaysAvatarItem = YES;
  1. Implement the ATLConversationListViewController delegate method avatarItemForConversation. The following example will return the avatar of the user of the last message.

    - (id<ATLAvatarItem>)conversationListViewController:(ATLConversationListViewController *)conversationListViewController avatarItemForConversation:(LYRConversation *)conversation
    {
        NSString *userID = conversation.lastMessage.sender.userID;
        // Get ATLParticipant for that userID 
        // [YourCode getATLParticipant] is pseudocode 
        ATLParticipant *lastUser = [YourCode getATLParticipant:userID];
        return user;
    }
    

Configuring UI Elements

Atlas takes advantage of Apple’s UIAppearance protocol which lets you change UI appearance very easily. You can change these elements in the viewDidLoad method of the relevant view controller. Here are a couple examples of common customizations:

To change the font of the message cell.

[[ATLMessageCollectionViewCell appearance] setMessageTextFont:[UIFont fontWithName:@"MarkerFelt-Thin" size:20.0f]];

To change the bubble color of the outgoing message cell.

[[ATLOutgoingMessageCollectionViewCell appearance] setMessageTextColor:[UIColor orangeColor]];

The following is a list of all Atlas properties conforming to UIAppearance:

ATLMessageCollectionViewCell

@property (nonatomic) UIFont *messageTextFont
@property (nonatomic) UIColor *messageTextColor
@property (nonatomic) UIColor *messageLinkTextColor
@property (nonatomic) UIColor *bubbleViewColor
@property (nonatomic) CGFloat bubbleViewCornerRadius

Note

ATLOutgoingMessageCollectionViewCell and ATLIncomingMessageCollectionViewCell extend this class

ATLAddressBarTextView

@property (nonatomic) UIFont *addressBarFont
@property (nonatomic) UIColor *addressBarTextColor
@property (nonatomic) UIColor *addressBarHighlightColor

ATLAvatarImageView

@property (nonatomic) CGFloat avatarImageViewDiameter
@property (nonatomic) UIFont *initialsFont
@property (nonatomic) UIColor *initialsColor
@property (nonatomic) UIColor *imageViewBackgroundColor

ATLConversationCollectionViewHeader

@property (nonatomic) UIFont *participantLabelFont
@property (nonatomic) UIColor *participantLabelTextColor

ATLConversationTableViewCell

@property (nonatomic) UIFont *conversationTitleLabelFont
@property (nonatomic) UIColor *conversationTitleLabelColor
@property (nonatomic) UIFont *lastMessageLabelFont
@property (nonatomic) UIColor *lastMessageLabelColor
@property (nonatomic) UIFont *dateLabelFont
@property (nonatomic) UIColor *dateLabelColor
@property (nonatomic) UIColor *unreadMessageIndicatorBackgroundColor
@property (nonatomic) UIColor *cellBackgroundColor

ATLParticipantSectionHeaderView

@property (nonatomic) UIFont *sectionHeaderFont
@property (nonatomic) UIColor *sectionHeaderTextColor
@property (nonatomic) UIColor *sectionHeaderBackgroundColor

ATLParticipantTableViewCell

@property (nonatomic) UIFont *titleFont
@property (nonatomic) UIFont *boldTitleFont
@property (nonatomic) UIColor *titleColor

Custom Components

Custom Message Type

A common feature to design and implement is the ability to send and receive a custom message type. For example, a “Product Information Card” could be sent that displays a product image, title, description, price, and Buy button all in the same message cell.

The following tutorial shows you how to do a simple version of this. You will:

  1. Determine if a message contains a single emoji to display bigger than usual
  2. Send a message containing the custom big emoji MIMEType
  3. Display a custom cell when rendering a message with this custom MIMEType

The end result of this tutorial looks like:

Image

Create custom UICollectionViewCell class

The custom cells in this tutorial will be named OutgoingBigEmojiCollectionViewCell and IncomingBigEmojiCollectionViewCell and will show an enlarged version of any single emoji character. The code provided is for the outgoing cells, and you’ll need to create a near-identical version for the incoming cells. In this example, the only difference is whether it displays on the left or right side of the view.

** Note **

This is a simple version for example purposes only and uses a few magic numbers to quickly render results. In a real app you’ll want to calculate sizes and positions dynamically for various devices.

  1. Your new custom class must implement ATLMessagePresenting

    @interface OutgoingBigEmojiCollectionViewCell : UICollectionViewCell <ATLMessagePresenting>
    
    @interface IncomingBigEmojiCollectionViewCell : UICollectionViewCell <ATLMessagePresenting>
    
  2. Override initWithFrame and perform your initial setup

    @interface OutgoingBigEmojiCollectionViewCell ()
    @property (strong,nonatomic) UILabel *emoji;
    @property (strong,nonatomic) LYRMessage *message;
    @end
    
    @implementation OutgoingBigEmojiCollectionViewCell
    
    -(instancetype)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if(self)
        {
            // Configure Label
            _emoji = [[UILabel alloc] init];
            _emoji.translatesAutoresizingMaskIntoConstraints = NO;
            _emoji.textAlignment = NSTextAlignmentRight;
            _emoji.font = [UIFont fontWithName:_emoji.font.fontName size:_emoji.font.pointSize * 3];
            [self addSubview:_emoji];
            [self configureConstraints];
        }
        return self;
    }
    
    - (void)configureConstraints {
        [self addConstraint:[NSLayoutConstraint constraintWithItem:self.emoji attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeWidth multiplier:0.5f constant:0]];
        [self addConstraint:[NSLayoutConstraint constraintWithItem:self.emoji attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeHeight multiplier:0.5f constant:0]];
        [self addConstraint:[NSLayoutConstraint constraintWithItem:self.emoji attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:0.5f constant:0]];
        [self addConstraint:[NSLayoutConstraint constraintWithItem:self.emoji attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:0.7f constant:0]];
    }
    
  3. Implement ATLMessagePresenting methods. updateWithSender and shouldDisplayAvatarItem are not used in this example so have them return immediately.

    - (void)updateWithSender:(id<ATLParticipant>)sender{return;}
    - (void)shouldDisplayAvatarItem:(BOOL)shouldDisplayAvatarItem{return;}
    

    presentMessage contains the message object associated with the cell. Retrieve the data from message parts and update the cell.

    - (void)presentMessage:(LYRMessage *)message {
        self.message = message;
        LYRMessagePart *part = message.parts[0];
    
        // If message contains custom mime type then get the text from the MessagePart JSON
        if([part.MIMEType isEqual: ATLMimeTypeBigEmoji])
        {
            NSData *data = part.data;
            NSError* error;
            NSDictionary* json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
            self.emoji.text = [json objectForKey:@"emoji"];
        }
    }
    

ATLMimeTypeBigEmoji is defined in the next step.

Configure ConversationViewController

  1. Define the custom cell and mimetype constants

    static NSString *const ATLMOutgoingBigEmojiReuseIdentifier = @"ATLMOutgoingBigEmojiReuseIdentifier";
    static NSString *const ATLMIncomingBigEmojiReuseIdentifier = @"ATLMIncomingBigEmojiReuseIdentifier";
    static NSString *const ATLMimeTypeBigEmoji = @"application/bigemoji";
    static NSString *const ATLMimeTypeBigEmojiInfo = @"application/bigemojiinfo";
    
  2. In viewDidLoad, register the custom cell classes. This tells the view controller that there’s a custom cell that could be used.

    // Register custom cell class for large emoji cell
    [self registerClass:[ATLMOutgoingBigEmojiCollectionViewCell class] forMessageCellWithReuseIdentifier:ATLMOutgoingBigEmojiReuseIdentifier];
    [self registerClass:[ATLMIncomingBigEmojiCollectionViewCell class] forMessageCellWithReuseIdentifier:ATLMIncomingBigEmojiReuseIdentifier];
    
  3. Implement the messagesForMediaAttachments datasource method. messagesForMediaAttachments is the method that gets called when you press the right accessory button before it sends the message. This is where you can configure the LYRMessages that get sent. In this example, we will create a message with 2 message parts with JSON blocks containing information to be displayed in the cell and information about the cell itself.

    - (NSOrderedSet *)conversationViewController:(ATLConversationViewController *)viewController messagesForMediaAttachments:(NSArray *)mediaAttachments
    {
        if (mediaAttachments.count == 1)
        {
            ATLMediaAttachment *mediaAttachment = mediaAttachments.firstObject;
            NSString *singleEmoji = [self singleEmojiFromMediaAttachment:mediaAttachment];
            if (singleEmoji) {
                // Create message part with the single emoji
                NSDictionary *dataDictionary = @{@"emoji":singleEmoji};
                NSError *JSONSerializerError;
                NSData *dataDictionaryJSON = [NSJSONSerialization dataWithJSONObject:dataDictionary options:NSJSONWritingPrettyPrinted error:&JSONSerializerError];
                LYRMessagePart *dataMessagePart = [LYRMessagePart messagePartWithMIMEType:ATLMimeTypeBigEmoji data:dataDictionaryJSON];
    
                // Create message part with info about cell
                NSDictionary *cellInfoDictionary = @{@"height":@"64"};
                NSData *cellInfoDictionaryJSON = [NSJSONSerialization dataWithJSONObject:cellInfoDictionary options:NSJSONWritingPrettyPrinted error:&JSONSerializerError];
                LYRMessagePart *cellInfoMessagePart = [LYRMessagePart messagePartWithMIMEType:ATLMimeTypeBigEmoji data:cellInfoDictionaryJSON];
    
                // Add message to ordered set.  This ordered set messages will get sent to the participants
                NSError *error;
                LYRMessage *message = [self.layerClient newMessageWithParts:@[dataMessagePart,cellInfoMessagePart] options:nil error:&error];
                NSOrderedSet *messageSet = [[NSOrderedSet alloc] initWithObject:message];
                return messageSet;
            }
        }
        return nil;
    }
    
  4. Implement a couple of helper methods for determining if a string contains only a single emoji. There are many ways to accomplish this, so the method textIsSingleEmoji is left open for you to design.

    - (NSString *)singleEmojiFromMediaAttachment:(ATLMediaAttachment *)mediaAttachment
    {
        return [self textIsSingleEmoji:mediaAttachment.textRepresentation] ? mediaAttachment.textRepresentation : nil;
    }
    
    - (BOOL)textIsSingleEmoji:(NSString *)text
    {
        // TODO: Implement a method to determine if a given string is a single emoji
    
        return YES;
    }
    
  5. Implement the reuseIdentifierForMessage datasource method. This is where we tell Atlas to use the custom cell when the message contains the custom mimetype.

    - (NSString *)conversationViewController:(ATLConversationViewController *)viewController reuseIdentifierForMessage:(LYRMessage *)message
    {
        LYRMessagePart *part = message.parts[0];
    
        // if message contains the custom mimetype, then return the custom cell reuse identifier
        if([part.MIMEType isEqual: ATLMimeTypeBigEmoji])
        {
            if ([message.sender isEqual: self.layerClient.authenticatedUser]) {
                return ATLMOutgoingBigEmojiReuseIdentifier;
            } else {
                return ATLMIncomingBigEmojiReuseIdentifier;
            }
        }
        return nil;
    }
    
  6. Implement the heightForMessage datasource method. The custom cell has a custom height that’s store in the MessagePart of the Message.

    - (CGFloat)conversationViewController:(ATLConversationViewController *)viewController heightForMessage:(LYRMessage *)message withCellWidth:(CGFloat)cellWidth
    {
        LYRMessagePart *part = message.parts[0];
    
        // if message contains the custom mimetype, then grab the cell info from the other message part
        if([part.MIMEType isEqual: ATLMimeTypeCustomObject]) {
            LYRMessagePart *cellMessagePart = message.parts[1];
            NSData *data = cellMessagePart.data;
            NSError* error;
            NSDictionary* json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
    
            // Grab the height value from the JSON
            NSString *height = [json objectForKey:@"height"];
            NSInteger heightInt = [height integerValue];
            return heightInt;
        }
        return 0;
    }
    

Configure ConversationListViewController

Implement the lastMessageTextForConversation datasource method. This will the configure the message inside conversationListViewController to show some custom text if the last message contained the custom mimetype:

- (NSString *)conversationListViewController:(ATLConversationListViewController *)conversationListViewController lastMessageTextForConversation:(LYRConversation *)conversation {
    LYRMessagePart *part = conversation.lastMessage.parts[0];
    
    if([part.MIMEType  isEqual: ATLMimeTypeBigEmoji]) {
        NSData *data = part.data;
        NSError* error;
        NSDictionary* json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
        return [NSString stringWithFormat:@"Big %@", [json objectForKey:@"emoji"]];
    }
    return nil;
}
Core Components Changelog