Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composable messages #764

Merged
merged 9 commits into from
Jan 19, 2022
Merged

Composable messages #764

merged 9 commits into from
Jan 19, 2022

Conversation

wiruzx
Copy link
Contributor

@wiruzx wiruzx commented Jan 13, 2022

Composable Message View Architecture

In this pull request I introduced a new way to setup chat messages ui.

We're going to make this a new default, but going to save compatibility with the old approach as well.

Please keep in mind, that the work on this feature is not done, and will be continued as following milestone: Composable Message Architecture

You can get the highest-level overview to this pr by looking at ChatItemPresenter's implementation.

It has a few dependencies, responsible for all of the work:

Binder

Establishes connection between View and ViewModel.

LayoutAssembler & ViewAssembler

Assembles View Hierarchy from the list of factories & given hierarchy.
We create the same hierarchy for LayoutProviders.

FactoryAggregate

Factory, to which we register all the factories required to create some part of the message.
Currently those are View Factory, ViewModel Factory and LayoutProvider Factory.

Why do we need those?

  1. Because of the cell reuse we need to have a way to create View and ViewModel Separately.
    The View is going to be reused between messages with the same View structure, while ViewModels are kept in memory per each loaded message.

  2. In order to make composition of reused views possible, we also needed to separate the view hierarchy.
    We're doing this right now by specifying parent and one or more children to View/Layout assebler:

assembler.register(child: childKey, parent: parentKey)
// or
assembler.register(children: [.init(firstChild), .init(secondChild)], parent: parentKey)

Currently we need to do it twice for View and for Layout. This is going to be fixed in #743

Type Safety

Views, ViewModels and Layout for the Views are created in different factories and at different times.
So in order to check that View is compatible with given ViewModel and given Layout could be applied to the View, I introduced a BindingKey which will help to make sure that types are matching.

When you register View, ViewModel and LayoutProvider factoreis into the FactoryAggregate, it checks that those 3 entities are compatible with each other.
It also returns a binding key with type information: BindingKey<View, ViewModel, LayoutProvider>, so it can be used in other functions where we need the type information.

@wiruzx wiruzx marked this pull request as ready for review January 13, 2022 16:35
DummyChatItemPresenter()
}

public func configure(withCollectionView collectionView: UICollectionView) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about keeping consistency with the above methods and rename this method to configure(with collectionView: UICollectionView)?
(Or change the other one. This also applies to the above method)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is part of public protocol, so not sure if it's a great idea to do renaming here.

I could rename the other one(configure(with collectionView: UICollectionView)), to match this function (configure(withCollectionView collectionView: UICollectionView)), but I'm going to remove the protocol completely: #761

}

public static func registerCells(for collectionView: UICollectionView, with reuseID: String) {
collectionView.register(Cell.self, forCellWithReuseIdentifier: reuseID)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this reuseIdentifier injected in the init?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the case, we should probably use the method above, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function has to be a static function, that's why we're passing reuseID from the outside.
I'm going to fix this in #761

}

// TODO: Unify viewModels instantiation. We need to create everything at once #743
private func makeViewModels() -> [Key: Any] {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do u think about using a lazy var instead? It would be a single line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've already created an issue almost exactly for that: #743
What do you think if I fix it there?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds great ;)

return viewModels
}

private func makeLayoutProviders() -> LayoutProviders {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

return layoutProviders
}

private func makeRootViewSizeProvider() -> AnySizeProvider {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

)
}

public let presenterType: ChatItemPresenterProtocol.Type = ChatItemPresenter<ChatItemType>.self

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Niiiice :)

// MARK: - Multiple

public protocol MultipleContainerViewProtocol {
func addChildren(children: [UIView])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do u think about removing Children from method signature and only keep it in the parameter?

}
}

extension CachingLayoutProvder: AnyCachedLayoutProvider {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo? CachingLayoutProvider

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

}

let fallbackItemPresenterFactory: ChatItemPresenterFactoryProtocol
if shouldUseNewMessageArchitecture {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we group these two if statements?

binder.registerBinding(for: first)
binder.registerBinding(for: second)

var assembler = ViewAssembler(root: bubble)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the view assembler make the same registrations as the layout assembler. Can we avoid this duplication somehow?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to tackle that one in #749

@wiruzx wiruzx merged commit 8d54785 into master Jan 19, 2022
@wiruzx wiruzx deleted the composable-messages branch January 19, 2022 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants