Skip to content

Commit

Permalink
Merge pull request #55 from zwang/feature-avatar
Browse files Browse the repository at this point in the history
Adds support for user avatars
  • Loading branch information
diegosanchezr committed Mar 12, 2016
2 parents 052c607 + c811ade commit 2733b83
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public protocol MessageViewModelProtocol: class { // why class? https://gist.git
var showsFailedIcon: Bool { get }
var date: String { get }
var status: MessageViewModelStatus { get }
var avatarImage: Observable<UIImage?> { set get }
}

public protocol DecoratedMessageViewModelProtocol: MessageViewModelProtocol {
Expand Down Expand Up @@ -78,6 +79,15 @@ extension DecoratedMessageViewModelProtocol {
public var showsFailedIcon: Bool {
return self.messageViewModel.showsFailedIcon
}

public var avatarImage: Observable<UIImage?> {
get {
return self.messageViewModel.avatarImage
}
set {
self.messageViewModel.avatarImage = newValue
}
}
}

public class MessageViewModel: MessageViewModelProtocol {
Expand All @@ -97,15 +107,18 @@ public class MessageViewModel: MessageViewModelProtocol {
public let dateFormatter: NSDateFormatter
public private(set) var messageModel: MessageModelProtocol

public init(dateFormatter: NSDateFormatter, showsTail: Bool, messageModel: MessageModelProtocol) {
public init(dateFormatter: NSDateFormatter, showsTail: Bool, messageModel: MessageModelProtocol, avatarImage: UIImage?) {
self.dateFormatter = dateFormatter
self.showsTail = showsTail
self.messageModel = messageModel
self.avatarImage = Observable<UIImage?>(avatarImage)
}

public var showsFailedIcon: Bool {
return self.status == .Failed
}

public var avatarImage: Observable<UIImage?>
}

public class MessageViewModelDefaultBuilder {
Expand All @@ -119,6 +132,7 @@ public class MessageViewModelDefaultBuilder {
}()

public func createMessageViewModel(message: MessageModelProtocol) -> MessageViewModelProtocol {
return MessageViewModel(dateFormatter: self.dynamicType.dateFormatter, showsTail: false, messageModel: message)
// Override to use default avatarImage
return MessageViewModel(dateFormatter: self.dynamicType.dateFormatter, showsTail: false, messageModel: message, avatarImage: nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,18 @@ import UIKit
import Chatto

public protocol BaseMessageCollectionViewCellStyleProtocol {
func avatarSize(viewModel viewModel: MessageViewModelProtocol) -> CGSize
func avatarVerticalAlignment(viewModel viewModel: MessageViewModelProtocol) -> VerticalAlignment
var failedIcon: UIImage { get }
var failedIconHighlighted: UIImage { get }
func attributedStringForDate(date: String) -> NSAttributedString
func layoutConstants(viewModel viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants
}

public struct BaseMessageCollectionViewCellLayoutConstants {
let horizontalMargin: CGFloat = 11
let horizontalInterspacing: CGFloat = 4
let maxContainerWidthPercentageForBubbleView: CGFloat = 0.68
let horizontalMargin: CGFloat
let horizontalInterspacing: CGFloat
let maxContainerWidthPercentageForBubbleView: CGFloat
}


Expand Down Expand Up @@ -101,12 +104,6 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
}
}

var layoutConstants = BaseMessageCollectionViewCellLayoutConstants() {
didSet {
self.setNeedsLayout()
}
}

public var canCalculateSizeInBackground: Bool {
return self.bubbleView.canCalculateSizeInBackground
}
Expand All @@ -117,6 +114,12 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
return nil
}

public private(set) var avatarView: UIImageView!
func createAvatarView() -> UIImageView! {
let avatarImageView = UIImageView(frame: CGRect.zero)
return avatarImageView
}

public override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
Expand All @@ -139,9 +142,11 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
}()

private func commonInit() {
self.avatarView = self.createAvatarView()
self.bubbleView = self.createBubbleView()
self.bubbleView.addGestureRecognizer(self.tapGestureRecognizer)
self.bubbleView.addGestureRecognizer(self.longPressGestureRecognizer)
self.contentView.addSubview(self.avatarView)
self.contentView.addSubview(self.bubbleView)
self.contentView.addSubview(self.failedButton)
self.contentView.exclusiveTouch = true
Expand Down Expand Up @@ -181,6 +186,10 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
self.failedButton.alpha = 0
}
self.accessoryTimestamp?.attributedText = style.attributedStringForDate(viewModel.date)
let avatarImageSize = baseStyle.avatarSize(viewModel: messageViewModel)
if avatarImageSize != CGSize.zero {
self.avatarView.image = self.messageViewModel.avatarImage.value
}
self.setNeedsLayout()
}

Expand All @@ -194,6 +203,8 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
self.bubbleView.preferredMaxLayoutWidth = layoutModel.preferredMaxWidthForBubble
self.bubbleView.layoutIfNeeded()

self.avatarView.bma_rect = layoutModel.avatarViewFrame

// TODO: refactor accessorView?

if let accessoryView = self.accessoryTimestamp {
Expand All @@ -217,15 +228,18 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
}

private func calculateLayout(availableWidth availableWidth: CGFloat) -> BaseMessageLayoutModel {
let layoutConstants = baseStyle.layoutConstants(viewModel: messageViewModel)
let parameters = BaseMessageLayoutModelParameters(
containerWidth: availableWidth,
horizontalMargin: self.layoutConstants.horizontalMargin,
horizontalInterspacing: self.layoutConstants.horizontalInterspacing,
horizontalMargin: layoutConstants.horizontalMargin,
horizontalInterspacing: layoutConstants.horizontalInterspacing,
failedButtonSize: self.failedIcon.size,
maxContainerWidthPercentageForBubbleView: self.layoutConstants.maxContainerWidthPercentageForBubbleView,
maxContainerWidthPercentageForBubbleView: layoutConstants.maxContainerWidthPercentageForBubbleView,
bubbleView: self.bubbleView,
isIncoming: self.messageViewModel.isIncoming,
isFailed: self.messageViewModel.showsFailedIcon
isFailed: self.messageViewModel.showsFailedIcon,
avatarSize: baseStyle.avatarSize(viewModel: messageViewModel),
avatarVerticalAlignment: baseStyle.avatarVerticalAlignment(viewModel: messageViewModel)
)
var layoutModel = BaseMessageLayoutModel()
layoutModel.calculateLayout(parameters: parameters)
Expand Down Expand Up @@ -311,8 +325,10 @@ struct BaseMessageLayoutModel {
private (set) var size = CGSize.zero
private (set) var failedViewFrame = CGRect.zero
private (set) var bubbleViewFrame = CGRect.zero
private (set) var avatarViewFrame = CGRect.zero
private (set) var preferredMaxWidthForBubble: CGFloat = 0


mutating func calculateLayout(parameters parameters: BaseMessageLayoutModelParameters) {
let containerWidth = parameters.containerWidth
let isIncoming = parameters.isIncoming
Expand All @@ -321,6 +337,7 @@ struct BaseMessageLayoutModel {
let bubbleView = parameters.bubbleView
let horizontalMargin = parameters.horizontalMargin
let horizontalInterspacing = parameters.horizontalInterspacing
let avatarSize = parameters.avatarSize

let preferredWidthForBubble = containerWidth * parameters.maxContainerWidthPercentageForBubbleView
let bubbleSize = bubbleView.sizeThatFits(CGSize(width: preferredWidthForBubble, height: CGFloat.max))
Expand All @@ -329,12 +346,17 @@ struct BaseMessageLayoutModel {

self.bubbleViewFrame = bubbleSize.bma_rect(inContainer: containerRect, xAlignament: .Center, yAlignment: .Center, dx: 0, dy: 0)
self.failedViewFrame = failedButtonSize.bma_rect(inContainer: containerRect, xAlignament: .Center, yAlignment: .Center, dx: 0, dy: 0)
self.avatarViewFrame = avatarSize.bma_rect(inContainer: containerRect, xAlignament: .Center, yAlignment: parameters.avatarVerticalAlignment, dx: 0, dy: 0)

// Adjust horizontal positions

var currentX: CGFloat = 0
if isIncoming {
currentX = horizontalMargin
self.avatarViewFrame.origin.x = currentX
currentX += avatarSize.width
currentX += horizontalInterspacing

if isFailed {
self.failedViewFrame.origin.x = currentX
currentX += failedButtonSize.width
Expand All @@ -345,6 +367,9 @@ struct BaseMessageLayoutModel {
self.bubbleViewFrame.origin.x = currentX
} else {
currentX = containerRect.maxX - horizontalMargin
currentX -= avatarSize.width
self.avatarViewFrame.origin.x = currentX
currentX -= horizontalInterspacing
if isFailed {
currentX -= failedButtonSize.width
self.failedViewFrame.origin.x = currentX
Expand All @@ -370,4 +395,6 @@ struct BaseMessageLayoutModelParameters {
let bubbleView: UIView
let isIncoming: Bool
let isFailed: Bool
let avatarSize: CGSize
let avatarVerticalAlignment: VerticalAlignment
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,23 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
lazy var baseColorOutgoing = UIColor.bma_color(rgb: 0x3D68F5)

lazy var borderIncomingTail: UIImage = {
return UIImage(named: "bubble-incoming-border-tail", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil)!
return UIImage(named: "bubble-incoming-border-tail", inBundle: NSBundle(forClass: BaseMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)!
}()

lazy var borderIncomingNoTail: UIImage = {
return UIImage(named: "bubble-incoming-border", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil)!
return UIImage(named: "bubble-incoming-border", inBundle: NSBundle(forClass: BaseMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)!
}()

lazy var borderOutgoingTail: UIImage = {
return UIImage(named: "bubble-outgoing-border-tail", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil)!
return UIImage(named: "bubble-outgoing-border-tail", inBundle: NSBundle(forClass: BaseMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)!
}()

lazy var borderOutgoingNoTail: UIImage = {
return UIImage(named: "bubble-outgoing-border", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil)!
return UIImage(named: "bubble-outgoing-border", inBundle: NSBundle(forClass: BaseMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)!
}()

public lazy var failedIcon: UIImage = {
return UIImage(named: "base-message-failed-icon", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil)!
return UIImage(named: "base-message-failed-icon", inBundle: NSBundle(forClass: BaseMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)!
}()

public lazy var failedIconHighlighted: UIImage = {
Expand Down Expand Up @@ -76,4 +76,19 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
return self.borderOutgoingNoTail
}
}

// Override this method to provide a size of avatarImage, so avatar image will be displayed if there is any in the viewModel
// if no image, then no avatar will be displayed, and a blank space will placehold at the position
public func avatarSize(viewModel viewModel: MessageViewModelProtocol) -> CGSize {
return CGSize.zero
}

// Specify the vertical alignment of the avatar image in the cell. By Default it is Botton, can go Top or Center
public func avatarVerticalAlignment(viewModel viewModel: MessageViewModelProtocol) -> VerticalAlignment {
return VerticalAlignment.Bottom
}

public func layoutConstants(viewModel viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants {
return BaseMessageCollectionViewCellLayoutConstants(horizontalMargin: 11, horizontalInterspacing: 4, maxContainerWidthPercentageForBubbleView: 0.68)
}
}
4 changes: 4 additions & 0 deletions ChattoApp/ChattoApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
C3F91DCC1C75EFE300D461D2 /* SendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F91DC91C75EFE300D461D2 /* SendingStatusCollectionViewCell.swift */; };
C3F91DCD1C75EFE300D461D2 /* SendingStatusCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C3F91DCA1C75EFE300D461D2 /* SendingStatusCollectionViewCell.xib */; };
C3F91DCE1C75EFE300D461D2 /* SendingStatusPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F91DCB1C75EFE300D461D2 /* SendingStatusPresenter.swift */; };
FE2D050B1C915ADB006F902B /* BaseMessageCollectionViewCellAvatarStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2D050A1C915ADB006F902B /* BaseMessageCollectionViewCellAvatarStyle.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -96,6 +97,7 @@
C3F91DC91C75EFE300D461D2 /* SendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
C3F91DCA1C75EFE300D461D2 /* SendingStatusCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SendingStatusCollectionViewCell.xib; sourceTree = "<group>"; };
C3F91DCB1C75EFE300D461D2 /* SendingStatusPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendingStatusPresenter.swift; sourceTree = "<group>"; };
FE2D050A1C915ADB006F902B /* BaseMessageCollectionViewCellAvatarStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseMessageCollectionViewCellAvatarStyle.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -208,6 +210,7 @@
C3F91DAC1C75EF9E00D461D2 /* Photo Messages */,
C3F91DC81C75EFE300D461D2 /* Sending status */,
C3F91DB11C75EF9E00D461D2 /* Text Messages */,
FE2D050A1C915ADB006F902B /* BaseMessageCollectionViewCellAvatarStyle.swift */,
);
path = Source;
sourceTree = "<group>";
Expand Down Expand Up @@ -440,6 +443,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FE2D050B1C915ADB006F902B /* BaseMessageCollectionViewCellAvatarStyle.swift in Sources */,
C3F91DBF1C75EF9E00D461D2 /* FakeMessageSender.swift in Sources */,
C3F91DB81C75EF9E00D461D2 /* ConversationsViewController.swift in Sources */,
C3F91DC41C75EF9E00D461D2 /* DemoTextMessageHandler.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "userAvatar.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.
6 changes: 3 additions & 3 deletions ChattoApp/ChattoApp/Base.lproj/LaunchScreen.storyboard
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8150" systemVersion="15A204g" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9532" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8122"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9530"/>
</dependencies>
<scenes>
<!--View Controller-->
Expand All @@ -15,7 +16,6 @@
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<animations/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
The MIT License (MIT)

Copyright (c) 2016-present Zhao Wang.

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 Foundation
import ChattoAdditions

class BaseMessageCollectionViewCellAvatarStyle: BaseMessageCollectionViewCellDefaultStyle {
override func avatarSize(viewModel viewModel: MessageViewModelProtocol) -> CGSize {
// Display avatar for both incoming and outgoing messages for demo purpose
return CGSize(width: 35, height: 35)
}
}
10 changes: 6 additions & 4 deletions ChattoApp/ChattoApp/Source/DemoChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,14 @@ class DemoChatViewController: BaseChatViewController {
}

override func createPresenterBuilders() -> [ChatItemType: [ChatItemPresenterBuilderProtocol]] {
let textMessagePresenter = TextMessagePresenterBuilder(
viewModelBuilder: DemoTextMessageViewModelBuilder(),
interactionHandler: DemoTextMessageHandler(baseHandler: self.baseMessageHandler)
)
textMessagePresenter.baseMessageStyle = BaseMessageCollectionViewCellAvatarStyle()
return [
DemoTextMessageModel.chatItemType: [
TextMessagePresenterBuilder(
viewModelBuilder: DemoTextMessageViewModelBuilder(),
interactionHandler: DemoTextMessageHandler(baseHandler: self.baseMessageHandler)
)
textMessagePresenter
],
DemoPhotoMessageModel.chatItemType: [
PhotoMessagePresenterBuilder(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public class DemoTextMessageViewModelBuilder: ViewModelBuilderProtocol {
public func createViewModel(textMessage: DemoTextMessageModel) -> DemoTextMessageViewModel {
let messageViewModel = self.messageViewModelBuilder.createMessageViewModel(textMessage)
let textMessageViewModel = DemoTextMessageViewModel(textMessage: textMessage, messageViewModel: messageViewModel)
// Best place to decide whether user having avart might be in the presenter. Here just for demo purpose
// Because we might only want to display avatar when showTail in the nessage bubble
textMessageViewModel.avatarImage.value = UIImage(named: "userAvatar")
return textMessageViewModel
}

Expand Down

0 comments on commit 2733b83

Please sign in to comment.