Skip to content

Commit

Permalink
Merge pull request #764 from badoo/composable-messages
Browse files Browse the repository at this point in the history
Composable messages
  • Loading branch information
wiruzx authored Jan 19, 2022
2 parents 1f4f9a7 + a1c6b49 commit 8d54785
Show file tree
Hide file tree
Showing 39 changed files with 3,284 additions and 701 deletions.
4 changes: 4 additions & 0 deletions Chatto/sources/Chat Items/ChatItemProtocolDefinitions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ public protocol ChatItemPresenterBuilderProtocol {
var presenterType: ChatItemPresenterProtocol.Type { get }
}

public protocol ChatItemPresenterBuilderCollectionViewConfigurable {
func configure(with collectionView: UICollectionView)
}

// MARK: - Updatable Chat Items

public protocol ContentEquatableChatItemProtocol: ChatItemProtocol {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,16 @@ public protocol ChatItemPresenterFactoryProtocol {
}

public final class ChatItemPresenterFactory: ChatItemPresenterFactoryProtocol {
let presenterBuildersByType: [ChatItemType: [ChatItemPresenterBuilderProtocol]]

public init(presenterBuildersByType: [ChatItemType: [ChatItemPresenterBuilderProtocol]]) {
public typealias PresenterBuildersByType = [ChatItemType: [ChatItemPresenterBuilderProtocol]]

private let presenterBuildersByType: PresenterBuildersByType
private let fallbackItemPresenterFactory: ChatItemPresenterFactoryProtocol

public init(presenterBuildersByType: PresenterBuildersByType,
fallbackItemPresenterFactory: ChatItemPresenterFactoryProtocol = DummyItemPresenterFactory()) {
self.presenterBuildersByType = presenterBuildersByType
self.fallbackItemPresenterFactory = fallbackItemPresenterFactory
}

public func createChatItemPresenter(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
Expand All @@ -42,13 +48,30 @@ public final class ChatItemPresenterFactory: ChatItemPresenterFactoryProtocol {
return builder.createPresenterWithChatItem(chatItem)
}
}
return DummyChatItemPresenter()
return self.fallbackItemPresenterFactory.createChatItemPresenter(chatItem)
}

public func configure(withCollectionView collectionView: UICollectionView) {
for presenterBuilder in self.presenterBuildersByType.flatMap({ $0.1 }) {
presenterBuilder.presenterType.registerCells(collectionView)
if let configurablePresenterBuilder = presenterBuilder as? ChatItemPresenterBuilderCollectionViewConfigurable {
configurablePresenterBuilder.configure(with: collectionView)
} else {
presenterBuilder.presenterType.registerCells(collectionView)
}
}
self.fallbackItemPresenterFactory.configure(withCollectionView: collectionView)
}
}

public final class DummyItemPresenterFactory: ChatItemPresenterFactoryProtocol {

public init() {}

public func createChatItemPresenter(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
DummyChatItemPresenter()
}

public func configure(withCollectionView collectionView: UICollectionView) {
DummyChatItemPresenter.registerCells(collectionView)
}
}
Expand Down
124 changes: 124 additions & 0 deletions ChattoAdditions/ChattoAdditions.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

public protocol BaseViewComposition {
associatedtype View
}

73 changes: 73 additions & 0 deletions ChattoAdditions/sources/Chat Items/Composable/Binder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@

// MARK: - Protocols

public protocol BinderProtocol {
typealias Key = AnyBindingKey
typealias Subviews = IndexedSubviews<Key>
typealias ViewModels = [Key: Any]

func bind(subviews: Subviews, viewModels: ViewModels) throws
}

public protocol BinderRegistryProtocol {
mutating func register<Binding: ViewModelBinding, Key: BindingKeyProtocol>(
binding: Binding,
for key: Key
) where Binding.View == Key.View, Binding.ViewModel == Key.ViewModel
}

// MARK: - Implementation

public struct Binder: BinderProtocol, BinderRegistryProtocol {

// MARK: - Type declarations

public enum Error: Swift.Error {
case keysMismatch
case noView(key: String)
case noViewModel(key: String)
}

// MARK: - Private properties

private var bindings: [AnyBindingKey: AnyViewModelBinding] = [:]

// MARK: - Instantiation

public init() {}

// MARK: - BinderRegistryProtocol

public mutating func register<Binding: ViewModelBinding, Key: BindingKeyProtocol>(binding: Binding, for key: Key)
where Binding.View == Key.View, Binding.ViewModel == Key.ViewModel {
self.bindings[.init(key)] = AnyViewModelBinding(binding)
}

// MARK: - BinderProtocol

public func bind(subviews: Subviews, viewModels: ViewModels) throws {
try self.checkKeys(subviews: subviews, viewModels: viewModels)

for (key, binding) in self.bindings {
guard let view = subviews.subviews[key] else {
throw Error.noView(key: key.description)
}
guard let viewModel = viewModels[key] else {
throw Error.noViewModel(key: key.description)
}
try binding.bind(view: view, to: viewModel)
}
}

// MARK: - Private

private func checkKeys(subviews: Subviews, viewModels: ViewModels) throws {
let bindingKeys = Set(bindings.keys)
let subviewsKeys = Set(subviews.subviews.keys)
let viewModelsKeys = Set(viewModels.keys)
guard bindingKeys == subviewsKeys && subviewsKeys == viewModelsKeys else {
throw Error.keysMismatch
}
}
}

64 changes: 64 additions & 0 deletions ChattoAdditions/sources/Chat Items/Composable/BindingKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

import Foundation

// MARK: - Protocol

public protocol BindingKeyProtocol: Hashable, CustomStringConvertible {
associatedtype View
associatedtype ViewModel
associatedtype LayoutProvider: LayoutProviderProtocol
var uuid: UUID { get }
}

// MARK: - Implementation

public struct BindingKey<View, ViewModel, LayoutProvider: LayoutProviderProtocol>: BindingKeyProtocol {
public let uuid = UUID()
public var description: String { "BindingKey<\(View.self),\(ViewModel.self),\(LayoutProvider.self)>" }
}

// MARK: - Type erasure

public struct AnyBindingKey: Hashable, CustomStringConvertible {

let uuid: UUID

// MARK: - Private properties

private let viewType: Any.Type
private let viewModelType: Any.Type
private let layoutProviderType: Any.Type
private let _description: () -> String

// MARK: - Instantiation

public init<Base: BindingKeyProtocol>(_ base: Base) {
self.viewType = Base.View.self
self.viewModelType = Base.ViewModel.self
self.layoutProviderType = Base.LayoutProvider.self
self.uuid = base.uuid
self._description = { base.description }
}

// MARK: - Hashable

public func hash(into hasher: inout Hasher) {
hasher.combine(self.uuid)
hasher.combine(ObjectIdentifier(self.viewType))
hasher.combine(ObjectIdentifier(self.viewModelType))
hasher.combine(ObjectIdentifier(self.layoutProviderType))
}

// MARK: - Equatable

public static func == (lhs: AnyBindingKey, rhs: AnyBindingKey) -> Bool {
return lhs.viewType == rhs.viewType
&& lhs.viewModelType == rhs.viewModelType
&& lhs.layoutProviderType == rhs.layoutProviderType
&& lhs.uuid == rhs.uuid
}

// MARK: - CustomStringConvertible

public var description: String { self._description() }
}
47 changes: 47 additions & 0 deletions ChattoAdditions/sources/Chat Items/Composable/ChatItemCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// The MIT License (MIT)
//
// Copyright (c) 2015-present Badoo Trading Limited.
//
// 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 UIKit

public final class ChatItemCell: UICollectionViewCell {

public var indexed: AnyIndexedSubviews? {
didSet {
guard oldValue == nil else { return }
guard let subviews = self.indexed else { fatalError() }
self.setup(subviews: subviews)
}
}

private func setup(subviews: AnyIndexedSubviews) {
let subview = subviews.root
subview.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(subview)
NSLayoutConstraint.activate([
subview.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
subview.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
subview.topAnchor.constraint(equalTo: self.contentView.topAnchor),
subview.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor)
])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

public protocol ChatItemContentView: AnyObject {
associatedtype ViewModel
func subscribe(for viewModel: ViewModel)
}

// MARK: - Automatic binding

private struct ChatItemContentViewModelBinding<View: ChatItemContentView>: ViewModelBinding {
func bind(view: View, to viewModel: View.ViewModel) {
view.subscribe(for: viewModel)
}
}

public extension Binder {
mutating func registerBinding<Key: BindingKeyProtocol>(for key: Key) where Key.View: ChatItemContentView, Key.ViewModel == Key.View.ViewModel {
self.register(binding: ChatItemContentViewModelBinding(), for: key)
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// The MIT License (MIT)
//
// Copyright (c) 2015-present Badoo Trading Limited.
//
// 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.

public protocol ChatItemLifecycleViewModel {
func willShow()
}
Loading

0 comments on commit 8d54785

Please sign in to comment.