Skip to content

Commit

Permalink
Merge pull request #67 from diegosanchezr/dev
Browse files Browse the repository at this point in the history
Improvements for custom styling
  • Loading branch information
Viacheslav-Radchenko committed Mar 22, 2016
2 parents 5c28e92 + c1b7cb7 commit 193fd45
Show file tree
Hide file tree
Showing 16 changed files with 374 additions and 132 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ disabled_rules: # rule identifiers to exclude from running
- type_body_length
- variable_name
- type_name
- function_parameter_count
11 changes: 9 additions & 2 deletions Chatto/Source/ChatController/AccessoryViewRevealer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
import Foundation

public protocol AccessoryViewRevealable {
func revealAccessoryView(maximumOffset offset: CGFloat, animated: Bool)
func revealAccessoryView(withOffset offset: CGFloat, animated: Bool)
func preferredOffsetToRevealAccessoryView() -> CGFloat? // This allows to sync size in case cells have different sizes for the accessory view. Nil -> no restriction
}

class AccessoryViewRevealer: NSObject, UIGestureRecognizerDelegate {
Expand Down Expand Up @@ -73,9 +74,15 @@ class AccessoryViewRevealer: NSObject, UIGestureRecognizerDelegate {
}

private func revealAccessoryView(atOffset offset: CGFloat) {
// Find max offset (cells can have slighlty different timestamp size ( 3.00 am vs 11.37 pm )
let cells: [AccessoryViewRevealable] = self.collectionView.visibleCells().filter({$0 is AccessoryViewRevealable}).map({$0 as! AccessoryViewRevealable})
let offset = min(offset, cells.reduce(0) { (current, cell) -> CGFloat in
return max(current, cell.preferredOffsetToRevealAccessoryView() ?? 0)
})

for cell in self.collectionView.visibleCells() {
if let cell = cell as? AccessoryViewRevealable {
cell.revealAccessoryView(maximumOffset: offset, animated: offset == 0)
cell.revealAccessoryView(withOffset: offset, animated: offset == 0)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import UIKit
import Chatto

public protocol BaseMessageCollectionViewCellStyleProtocol {
func avatarSize(viewModel viewModel: MessageViewModelProtocol) -> CGSize
func avatarSize(viewModel viewModel: MessageViewModelProtocol) -> CGSize // .zero => no avatar
func avatarVerticalAlignment(viewModel viewModel: MessageViewModelProtocol) -> VerticalAlignment
var failedIcon: UIImage { get }
var failedIconHighlighted: UIImage { get }
Expand Down Expand Up @@ -191,7 +191,7 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
} else {
self.failedButton.alpha = 0
}
self.accessoryTimestamp?.attributedText = style.attributedStringForDate(viewModel.date)
self.accessoryTimestampView.attributedText = style.attributedStringForDate(viewModel.date)
let avatarImageSize = baseStyle.avatarSize(viewModel: messageViewModel)
if avatarImageSize != CGSize.zero {
self.avatarView.image = self.messageViewModel.avatarImage.value
Expand All @@ -211,21 +211,20 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U

self.avatarView.bma_rect = layoutModel.avatarViewFrame

// TODO: refactor accessorView?

if let accessoryView = self.accessoryTimestamp {
accessoryView.bounds = CGRect(origin: CGPoint.zero, size: accessoryView.intrinsicContentSize())
let accessoryViewWidth = CGRectGetWidth(accessoryView.bounds)
let accessoryViewMargin: CGFloat = 10
let leftDisplacement = max(0, min(self.timestampMaxVisibleOffset, accessoryViewWidth + accessoryViewMargin))
if self.accessoryTimestampView.superview != nil {
let layoutConstants = baseStyle.layoutConstants(viewModel: messageViewModel)
self.accessoryTimestampView.bounds = CGRect(origin: CGPoint.zero, size: self.accessoryTimestampView.intrinsicContentSize())
let accessoryViewWidth = CGRectGetWidth(self.accessoryTimestampView.bounds)
let leftOffsetForContentView = max(0, offsetToRevealAccessoryView)
let leftOffsetForAccessoryView = min(leftOffsetForContentView, accessoryViewWidth + layoutConstants.horizontalMargin)
var contentViewframe = self.contentView.frame
if self.messageViewModel.isIncoming {
contentViewframe.origin = CGPoint.zero
} else {
contentViewframe.origin.x = -leftDisplacement
contentViewframe.origin.x = -leftOffsetForContentView
}
self.contentView.frame = contentViewframe
accessoryView.center = CGPoint(x: CGRectGetWidth(self.bounds) - leftDisplacement + accessoryViewWidth / 2, y: self.contentView.center.y)
self.accessoryTimestampView.center = CGPoint(x: CGRectGetWidth(self.bounds) - leftOffsetForAccessoryView + accessoryViewWidth / 2, y: self.contentView.center.y)
}
}

Expand Down Expand Up @@ -254,53 +253,52 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U


// MARK: timestamp revealing
var timestampMaxVisibleOffset: CGFloat = 0 {

lazy var accessoryTimestampView = UILabel()

var offsetToRevealAccessoryView: CGFloat = 0 {
didSet {
self.setNeedsLayout()
}
}
var accessoryTimestamp: UILabel?
public func revealAccessoryView(maximumOffset offset: CGFloat, animated: Bool) {
if self.accessoryTimestamp == nil {


public func preferredOffsetToRevealAccessoryView() -> CGFloat? {
let layoutConstants = baseStyle.layoutConstants(viewModel: messageViewModel)
return self.accessoryTimestampView.intrinsicContentSize().width + layoutConstants.horizontalMargin
}


public func revealAccessoryView(withOffset offset: CGFloat, animated: Bool) {
self.offsetToRevealAccessoryView = offset
if self.accessoryTimestampView.superview == nil {
if offset > 0 {
let accessoryTimestamp = UILabel()
accessoryTimestamp.attributedText = self.baseStyle?.attributedStringForDate(self.messageViewModel.date)
self.addSubview(accessoryTimestamp)
self.accessoryTimestamp = accessoryTimestamp
self.addSubview(self.accessoryTimestampView)
self.layoutIfNeeded()
}

if animated {
UIView.animateWithDuration(self.animationDuration, animations: { () -> Void in
self.timestampMaxVisibleOffset = offset
self.layoutIfNeeded()
})
} else {
self.timestampMaxVisibleOffset = offset
}
} else {
if animated {
UIView.animateWithDuration(self.animationDuration, animations: { () -> Void in
self.timestampMaxVisibleOffset = offset
self.layoutIfNeeded()
}, completion: { (finished) -> Void in
if offset == 0 {
self.removeAccessoryView()
}
})

} else {
self.timestampMaxVisibleOffset = offset
}
}
}

func removeAccessoryView() {
self.accessoryTimestamp?.removeFromSuperview()
self.accessoryTimestamp = nil
self.accessoryTimestampView.removeFromSuperview()
}


// MARK: User interaction
public var onFailedButtonTapped: ((cell: BaseMessageCollectionViewCell) -> Void)?
@objc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,45 +26,110 @@ import UIKit

public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewCellStyleProtocol {

public init () {}
typealias Class = BaseMessageCollectionViewCellDefaultStyle

public struct Colors {
let incoming: () -> UIColor
let outgoing: () -> UIColor
public init(
@autoclosure(escaping) incoming: () -> UIColor,
@autoclosure(escaping) outgoing: () -> UIColor) {
self.incoming = incoming
self.outgoing = outgoing
}
}

lazy var baseColorIncoming = UIColor.bma_color(rgb: 0xE6ECF2)
lazy var baseColorOutgoing = UIColor.bma_color(rgb: 0x3D68F5)
public struct BubbleBorderImages {
let borderIncomingTail: () -> UIImage
let borderIncomingNoTail: () -> UIImage
let borderOutgoingTail: () -> UIImage
let borderOutgoingNoTail: () -> UIImage
public init(
@autoclosure(escaping) borderIncomingTail: () -> UIImage,
@autoclosure(escaping) borderIncomingNoTail: () -> UIImage,
@autoclosure(escaping) borderOutgoingTail: () -> UIImage,
@autoclosure(escaping) borderOutgoingNoTail: () -> UIImage) {
self.borderIncomingTail = borderIncomingTail
self.borderIncomingNoTail = borderIncomingNoTail
self.borderOutgoingTail = borderOutgoingTail
self.borderOutgoingNoTail = borderOutgoingNoTail
}
}

lazy var borderIncomingTail: UIImage = {
return UIImage(named: "bubble-incoming-border-tail", inBundle: NSBundle(forClass: BaseMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)!
}()
public struct FailedIconImages {
let normal: () -> UIImage
let highlighted: () -> UIImage
public init(
@autoclosure(escaping) normal: () -> UIImage,
@autoclosure(escaping) highlighted: () -> UIImage) {
self.normal = normal
self.highlighted = highlighted
}
}

lazy var borderIncomingNoTail: UIImage = {
return UIImage(named: "bubble-incoming-border", inBundle: NSBundle(forClass: BaseMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)!
}()
public struct DateTextStyle {
let font: () -> UIFont
let color: () -> UIColor
public init(
@autoclosure(escaping) font: () -> UIFont,
@autoclosure(escaping) color: () -> UIColor) {
self.font = font
self.color = color
}
}

lazy var borderOutgoingTail: UIImage = {
return UIImage(named: "bubble-outgoing-border-tail", inBundle: NSBundle(forClass: BaseMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)!
}()
public struct AvatarStyle {
let size: CGSize
let alignment: VerticalAlignment
public init(size: CGSize = .zero, alignment: VerticalAlignment = .Bottom) {
self.size = size
self.alignment = alignment
}
}

lazy var borderOutgoingNoTail: UIImage = {
return UIImage(named: "bubble-outgoing-border", inBundle: NSBundle(forClass: BaseMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)!
}()
let colors: Colors
let bubbleBorderImages: BubbleBorderImages
let failedIconImages: FailedIconImages
let layoutConstants: BaseMessageCollectionViewCellLayoutConstants
let dateTextStyle: DateTextStyle
let avatarStyle: AvatarStyle
public init (
colors: Colors = Class.createDefaultColors(),
bubbleBorderImages: BubbleBorderImages = Class.createDefaultBubbleBorderImages(),
failedIconImages: FailedIconImages = Class.createDefaultFailedIconImages(),
layoutConstants: BaseMessageCollectionViewCellLayoutConstants = Class.createDefaultLayoutConstants(),
dateTextStyle: DateTextStyle = Class.createDefaultDateTextStyle(),
avatarStyle: AvatarStyle = AvatarStyle()) {
self.colors = colors
self.bubbleBorderImages = bubbleBorderImages
self.failedIconImages = failedIconImages
self.layoutConstants = layoutConstants
self.dateTextStyle = dateTextStyle
self.avatarStyle = avatarStyle
}

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

public lazy var failedIconHighlighted: UIImage = {
return self.failedIcon.bma_blendWithColor(UIColor.blackColor().colorWithAlphaComponent(0.10))
}()
public lazy var borderIncomingTail: UIImage = self.bubbleBorderImages.borderIncomingTail()
public lazy var borderIncomingNoTail: UIImage = self.bubbleBorderImages.borderIncomingNoTail()
public lazy var borderOutgoingTail: UIImage = self.bubbleBorderImages.borderOutgoingTail()
public lazy var borderOutgoingNoTail: UIImage = self.bubbleBorderImages.borderOutgoingNoTail()

private lazy var dateFont = {
return UIFont.systemFontOfSize(12.0)
}()
public lazy var failedIcon: UIImage = self.failedIconImages.normal()
public lazy var failedIconHighlighted: UIImage = self.failedIconImages.highlighted()
private lazy var dateFont: UIFont = self.dateTextStyle.font()
private lazy var dateFontColor: UIColor = self.dateTextStyle.color()

public func attributedStringForDate(date: String) -> NSAttributedString {
let attributes = [NSFontAttributeName : self.dateFont]
let attributes = [
NSFontAttributeName : self.dateFont,
NSForegroundColorAttributeName: self.dateFontColor
]
return NSAttributedString(string: date, attributes: attributes)
}

func borderImage(viewModel viewModel: MessageViewModelProtocol) -> UIImage? {
public func borderImage(viewModel viewModel: MessageViewModelProtocol) -> UIImage? {
switch (viewModel.isIncoming, viewModel.showsTail) {
case (true, true):
return self.borderIncomingTail
Expand All @@ -77,18 +142,48 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
}
}

// 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
return self.avatarStyle.size
}

// 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
return self.avatarStyle.alignment
}

public func layoutConstants(viewModel viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants {
return self.layoutConstants
}
}

public extension BaseMessageCollectionViewCellDefaultStyle { // Default values
static public func createDefaultColors() -> Colors {
return Colors(incoming: UIColor.bma_color(rgb: 0xE6ECF2), outgoing: UIColor.bma_color(rgb: 0x3D68F5))
}

static public func createDefaultBubbleBorderImages() -> BubbleBorderImages {
return BubbleBorderImages(
borderIncomingTail: UIImage(named: "bubble-incoming-border-tail", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
borderIncomingNoTail: UIImage(named: "bubble-incoming-border", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
borderOutgoingTail: UIImage(named: "bubble-outgoing-border-tail", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
borderOutgoingNoTail: UIImage(named: "bubble-outgoing-border", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!
)
}

static public func createDefaultFailedIconImages() -> FailedIconImages {
let normal = {
return UIImage(named: "base-message-failed-icon", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!
}
return FailedIconImages(
normal: normal(),
highlighted: normal().bma_blendWithColor(UIColor.blackColor().colorWithAlphaComponent(0.10))
)
}

static public func createDefaultDateTextStyle() -> DateTextStyle {
return DateTextStyle(font: UIFont.systemFontOfSize(12), color: UIColor.bma_color(rgb: 0x9aa3ab))
}

static public func createDefaultLayoutConstants() -> BaseMessageCollectionViewCellLayoutConstants {
return BaseMessageCollectionViewCellLayoutConstants(horizontalMargin: 11, horizontalInterspacing: 4, maxContainerWidthPercentageForBubbleView: 0.68)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
public typealias ViewModelT = ViewModelBuilderT.ViewModelT

public let photoCellStyle: PhotoMessageCollectionViewCellStyleProtocol

public init (
messageModel: ModelT,
viewModelBuilder: ViewModelBuilderT,
Expand Down
Loading

0 comments on commit 193fd45

Please sign in to comment.