Skip to content

Commit

Permalink
Add safe area insets for decoration views
Browse files Browse the repository at this point in the history
  • Loading branch information
wiruzx committed Jul 29, 2020
1 parent 6379644 commit 24d1c95
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,12 @@ open class CompoundMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
private var layoutProvider: CompoundBubbleLayoutProvider {
let configuration: CompoundBubbleLayoutProvider.Configuration = {
let contentLayoutProviders = self.contentFactories.map { $0.createLayoutProvider(forModel: self.messageModel) }
let decorationLayoutProviders = self.decorationFactories.map { $0.makeLayoutProvider(for: self.messageModel) }
let viewModel = self.messageViewModel
let tailWidth = self.compoundCellStyle.tailWidth(forViewModel: viewModel)
return CompoundBubbleLayoutProvider.Configuration(
layoutProviders: contentLayoutProviders,
decorationLayoutProviders: decorationLayoutProviders,
tailWidth: tailWidth,
isIncoming: viewModel.isIncoming
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,52 @@ public protocol MessageManualLayoutProviderProtocol: HashableRepresentible {

// MARK: - Text

public struct TextMessageLayoutProvider: Hashable, MessageManualLayoutProviderProtocol {
public struct TextMessageLayout {
public let frame: CGRect
public let size: CGSize

public init(frame: CGRect, size: CGSize) {
self.frame = frame
self.size = size
}
}

public protocol TextMessageLayoutProviderProtocol: MessageManualLayoutProviderProtocol {
func layout(for size: CGSize, safeAreaInsets: UIEdgeInsets) -> TextMessageLayout
}

extension TextMessageLayoutProviderProtocol {
public func sizeThatFits(size: CGSize, safeAreaInsets: UIEdgeInsets) -> CGSize {
self.layout(for: size, safeAreaInsets: safeAreaInsets).size
}
}

public struct TextMessageLayoutProvider: Hashable, TextMessageLayoutProviderProtocol {

private let text: String
private let font: UIFont
private let textInsets: UIEdgeInsets
private let textInsetsFromSafeArea: UIEdgeInsets?
private let numberOfLines: Int

public init(text: String, font: UIFont, textInsets: UIEdgeInsets, numberOfLines: Int = 0) {
public init(text: String,
font: UIFont,
textInsets: UIEdgeInsets,
textInsetsFromSafeArea: UIEdgeInsets? = nil,
numberOfLines: Int = 0) {
self.text = text
self.font = font
self.textInsets = textInsets
self.textInsetsFromSafeArea = textInsetsFromSafeArea
self.numberOfLines = numberOfLines
}

public func sizeThatFits(size: CGSize, safeAreaInsets: UIEdgeInsets) -> CGSize {
public func layout(for size: CGSize, safeAreaInsets: UIEdgeInsets) -> TextMessageLayout {
let textInsets = self.textInsets(for: safeAreaInsets)
let combinedInsets = safeAreaInsets + textInsets
var sizeWithInset = size
sizeWithInset.substract(insets: safeAreaInsets)
sizeWithInset.substract(insets: self.textInsets)
sizeWithInset.substract(insets: combinedInsets)

let textContainer = NSTextContainer(size: sizeWithInset)
textContainer.lineFragmentPadding = 0
textContainer.maximumNumberOfLines = self.numberOfLines
Expand All @@ -61,10 +89,27 @@ public struct TextMessageLayoutProvider: Hashable, MessageManualLayoutProviderPr
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
var resultSize = layoutManager.usedRect(for: textContainer).size.bma_round()
resultSize.add(insets: safeAreaInsets)
resultSize.add(insets: self.textInsets)
return resultSize
let textSize = layoutManager.usedRect(for: textContainer).size.bma_round()
var resultSize = textSize
resultSize.add(insets: combinedInsets)

return TextMessageLayout(
frame: CGRect(
origin: combinedInsets.origin,
size: textSize
),
size: resultSize
)
}

private func textInsets(for safeAreaInsets: UIEdgeInsets) -> UIEdgeInsets {
guard let insetsFromSafeArea = self.textInsetsFromSafeArea else { return self.textInsets }
var textInsets = self.textInsets
if safeAreaInsets.top > 0 { textInsets.top = insetsFromSafeArea.top }
if safeAreaInsets.left > 0 { textInsets.left = insetsFromSafeArea.left }
if safeAreaInsets.right > 0 { textInsets.right = insetsFromSafeArea.right }
if safeAreaInsets.bottom > 0 { textInsets.bottom = insetsFromSafeArea.bottom }
return textInsets
}
}

Expand Down Expand Up @@ -96,3 +141,17 @@ private extension CGSize {
self.height -= insets.top + insets.bottom
}
}

private extension UIEdgeInsets {

var origin: CGPoint { CGPoint(x: self.left, y: self.top) }

static func + (lhs: UIEdgeInsets, rhs: UIEdgeInsets) -> UIEdgeInsets {
UIEdgeInsets(
top: lhs.top + rhs.top,
left: lhs.left + rhs.left,
bottom: lhs.bottom + rhs.bottom,
right: lhs.right + rhs.right
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public struct MessageDecorationViewLayout {
}
}

public protocol MessageDecorationViewLayoutProviderProtocol {
public protocol MessageDecorationViewLayoutProviderProtocol: HashableRepresentible {
func makeLayout(from bubbleBounds: CGRect) -> MessageDecorationViewLayout
var safeAreaInsets: UIEdgeInsets { get }
}

extension MessageDecorationViewLayoutProviderProtocol {
public var safeAreaInsets: UIEdgeInsets { .zero }
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,31 @@ public struct CompoundBubbleLayoutProvider {
public struct Configuration: Hashable {

fileprivate let layoutProviders: [MessageManualLayoutProviderProtocol]
fileprivate let decorationLayoutProviders: [MessageDecorationViewLayoutProviderProtocol]
fileprivate let tailWidth: CGFloat
fileprivate let isIncoming: Bool

public init(layoutProviders: [MessageManualLayoutProviderProtocol],
decorationLayoutProviders: [MessageDecorationViewLayoutProviderProtocol],
tailWidth: CGFloat,
isIncoming: Bool) {
self.layoutProviders = layoutProviders
self.decorationLayoutProviders = decorationLayoutProviders
self.tailWidth = tailWidth
self.isIncoming = isIncoming
}

public func hash(into hasher: inout Hasher) {
hasher.combine(self.layoutProviders.map { $0.asHashable })
hasher.combine(self.layoutProviders.map(\.asHashable))
hasher.combine(self.decorationLayoutProviders.map(\.asHashable))
hasher.combine(self.tailWidth)
hasher.combine(self.isIncoming)
}

public static func == (lhs: CompoundBubbleLayoutProvider.Configuration,
rhs: CompoundBubbleLayoutProvider.Configuration) -> Bool {
return lhs.layoutProviders.map { $0.asHashable } == rhs.layoutProviders.map { $0.asHashable }
return lhs.layoutProviders.map(\.asHashable) == rhs.layoutProviders.map(\.asHashable)
&& lhs.decorationLayoutProviders.map(\.asHashable) == rhs.decorationLayoutProviders.map(\.asHashable)
&& lhs.tailWidth == rhs.tailWidth
&& lhs.isIncoming == rhs.isIncoming
}
Expand Down Expand Up @@ -108,13 +113,26 @@ public struct CompoundBubbleLayoutProvider {
}

private func safeAreaInsets() -> UIEdgeInsets {
var left: CGFloat = 0
var right: CGFloat = 0
var insets: UIEdgeInsets = .zero
if self.configuration.isIncoming {
left = self.configuration.tailWidth
insets.left = self.configuration.tailWidth
} else {
right = self.configuration.tailWidth
insets.right = self.configuration.tailWidth
}
return UIEdgeInsets(top: 0, left: left, bottom: 0, right: right)

for provider in self.configuration.decorationLayoutProviders {
insets.combine(with: provider.safeAreaInsets)
}

return insets
}
}

private extension UIEdgeInsets {
mutating func combine(with other: UIEdgeInsets) {
self.top = max(self.top, other.top)
self.left = max(self.left, other.left)
self.right = max(self.right, other.right)
self.bottom = max(self.bottom, other.bottom)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ final class DemoEmojiDecorationViewFactory: MessageDecorationViewFactoryProtocol
}
}

private struct DemoEmojiDecorationViewLayoutProvider: MessageDecorationViewLayoutProviderProtocol {
private final class DemoEmojiDecorationViewLayoutProvider: Hashable, MessageDecorationViewLayoutProviderProtocol {

private let emoji: String
private let font: UIFont
private let isIncoming: Bool
Expand All @@ -58,10 +59,7 @@ private struct DemoEmojiDecorationViewLayoutProvider: MessageDecorationViewLayou
}

func makeLayout(from bubbleBounds: CGRect) -> MessageDecorationViewLayout {
let textLayoutProvider = TextMessageLayoutProvider(text: self.emoji,
font: self.font,
textInsets: .zero)
let size = textLayoutProvider.sizeThatFits(size: bubbleBounds.size, safeAreaInsets: .zero)
let size = self.emojiSize
return MessageDecorationViewLayout(
frame: .init(
origin: .init(
Expand All @@ -72,4 +70,35 @@ private struct DemoEmojiDecorationViewLayoutProvider: MessageDecorationViewLayou
)
)
}

var safeAreaInsets: UIEdgeInsets {
let width = self.emojiSize.width
let horizontalInset = width / 2
var insets: UIEdgeInsets = .zero
if self.isIncoming {
insets.right = horizontalInset
} else {
insets.left = horizontalInset
}
return insets
}

private lazy var emojiSize: CGSize = {
let textLayoutProvider = TextMessageLayoutProvider(text: self.emoji,
font: self.font,
textInsets: .zero)
return textLayoutProvider.sizeThatFits(size: UIView.layoutFittingExpandedSize, safeAreaInsets: .zero)
}()

static func == (lhs: DemoEmojiDecorationViewLayoutProvider, rhs: DemoEmojiDecorationViewLayoutProvider) -> Bool {
return lhs.emoji == rhs.emoji
&& lhs.font == rhs.font
&& lhs.isIncoming == rhs.isIncoming
}

func hash(into hasher: inout Hasher) {
hasher.combine(self.emoji)
hasher.combine(self.font)
hasher.combine(self.isIncoming)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,45 +35,63 @@ struct DemoTextMessageContentFactory: MessageContentFactoryProtocol {
}

func createContentView() -> UIView {
let label = LabelWithInsets()
label.numberOfLines = 0
label.font = self.font
label.textInsets = self.textInsets
return label
let textView = TextView()
textView.label.numberOfLines = 0
textView.label.font = self.font
return textView
}

func createContentPresenter(forModel model: DemoCompoundMessageModel) -> MessageContentPresenterProtocol {
return DefaultMessageContentPresenter<DemoCompoundMessageModel, LabelWithInsets>(
let layoutProvider = self.createTextLayoutProvider(forModel: model)
return DefaultMessageContentPresenter<DemoCompoundMessageModel, TextView>(
message: model,
showBorder: false,
onBinding: { message, label in
label?.text = message.text
label?.textColor = message.isIncoming ? .black : .white
onBinding: { message, textView in
guard let textView = textView else { return }
textView.label.text = message.text
textView.label.textColor = message.isIncoming ? .black : .white
textView.layoutProvider = layoutProvider
}
)
}

func createLayoutProvider(forModel model: DemoCompoundMessageModel) -> MessageManualLayoutProviderProtocol {
return TextMessageLayoutProvider(text: model.text,
font: self.font,
textInsets: self.textInsets)
self.createTextLayoutProvider(forModel: model)
}

func createMenuPresenter(forModel model: DemoCompoundMessageModel) -> ChatItemMenuPresenterProtocol? {
return nil
}
}

private final class LabelWithInsets: UILabel {
var textInsets: UIEdgeInsets = .zero
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: self.textInsets + self.safeAreaInsets))
private func createTextLayoutProvider(forModel model: DemoCompoundMessageModel) -> TextMessageLayoutProviderProtocol {
TextMessageLayoutProvider(text: model.text,
font: self.font,
textInsets: self.textInsets)
}
}

private func + (lhs: UIEdgeInsets, rhs: UIEdgeInsets) -> UIEdgeInsets {
return UIEdgeInsets(top: lhs.top + rhs.top,
left: lhs.left + rhs.left,
bottom: lhs.bottom + rhs.bottom,
right: lhs.right + rhs.right)
private final class TextView: UIView {

let label = UILabel()
var layoutProvider: TextMessageLayoutProviderProtocol? {
didSet {
guard self.layoutProvider != nil else { return }
self.setNeedsLayout()
}
}

init() {
super.init(frame: .zero)
self.addSubview(label)
}

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

override func layoutSubviews() {
super.layoutSubviews()
guard let layoutProvider = self.layoutProvider else { return }
self.label.frame = layoutProvider.layout(for: self.bounds.size, safeAreaInsets: self.safeAreaInsets).frame
}
}

0 comments on commit 24d1c95

Please sign in to comment.