diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/CollectionView.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/CollectionView.xcscheme
new file mode 100644
index 0000000000000000000000000000000000000000..69208709b4458ff0057e77ddada42bc9d39e7f52
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/CollectionView.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1340"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "CollectionView"
+               BuildableName = "CollectionView"
+               BlueprintName = "CollectionView"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      codeCoverageEnabled = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "CollectionViewTests"
+               BuildableName = "CollectionViewTests"
+               BlueprintName = "CollectionViewTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "CollectionView"
+            BuildableName = "CollectionView"
+            BlueprintName = "CollectionView"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Package.swift b/Package.swift
index 0e7bb614d299a3ed2eadd9464518af68ad563f9f..9317c5bef0efb8db66aabe1cb1485b61fd2426f6 100644
--- a/Package.swift
+++ b/Package.swift
@@ -35,6 +35,7 @@ let package = Package(
         .library(name: "iCloudFeature", targets: ["iCloudFeature"]),
         .library(name: "SearchFeature", targets: ["SearchFeature"]),
         .library(name: "DrawerFeature", targets: ["DrawerFeature"]),
+        .library(name: "CollectionView", targets: ["CollectionView"]),
         .library(name: "RestoreFeature", targets: ["RestoreFeature"]),
         .library(name: "CrashReporting", targets: ["CrashReporting"]),
         .library(name: "ProfileFeature", targets: ["ProfileFeature"]),
@@ -70,7 +71,9 @@ let package = Package(
         .package(url: "https://git.xx.network/elixxir/client-ios-db.git", .upToNextMajor(from: "1.0.5")),
         .package(url: "https://github.com/firebase/firebase-ios-sdk.git", .upToNextMajor(from: "8.10.0")),
         .package(url: "https://github.com/darrarski/Shout.git", revision: "df5a662293f0ac15eeb4f2fd3ffd0c07b73d0de0"),
-        .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git",.upToNextMajor(from: "0.32.0"))
+        .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git",.upToNextMajor(from: "0.32.0")),
+        .package(url: "https://github.com/pointfreeco/swift-custom-dump.git", .upToNextMajor(from: "0.5.0")),
+        .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git", .upToNextMajor(from: "0.3.3")),
     ],
     targets: [
         .target(
@@ -857,6 +860,23 @@ let package = Package(
                     .product(name: "Quick", package: "Quick"),
                     .product(name: "Nimble", package: "Nimble")
                 ]
-            )
+            ),
+
+        // MARK: - CollectionView
+
+            .target(
+                name: "CollectionView",
+                dependencies: [
+                    .product(name: "ChatLayout", package: "ChatLayout"),
+                    .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
+                ]
+            ),
+            .testTarget(
+                name: "CollectionViewTests",
+                dependencies: [
+                    .target(name: "CollectionView"),
+                    .product(name: "CustomDump", package: "swift-custom-dump"),
+                ]
+            ),
     ]
 )
diff --git a/Sources/CollectionView/CellFactory.swift b/Sources/CollectionView/CellFactory.swift
new file mode 100644
index 0000000000000000000000000000000000000000..bc7bc9bec131565d07dc5805865bdcc13e0959dc
--- /dev/null
+++ b/Sources/CollectionView/CellFactory.swift
@@ -0,0 +1,76 @@
+import UIKit
+import XCTestDynamicOverlay
+
+public struct CellFactory<Model> {
+  public struct Registrar {
+    public init(register: @escaping (UICollectionView) -> Void) {
+      self.register = register
+    }
+
+    public var register: (UICollectionView) -> Void
+
+    public func callAsFunction(in view: UICollectionView) {
+      register(view)
+    }
+  }
+
+  public struct Builder {
+    public init(build: @escaping (Model, UICollectionView, IndexPath) -> UICollectionViewCell?) {
+      self.build = build
+    }
+
+    public var build: (Model, UICollectionView, IndexPath) -> UICollectionViewCell?
+
+    public func callAsFunction(
+      for model: Model,
+      in view: UICollectionView,
+      at indexPath: IndexPath
+    ) -> UICollectionViewCell? {
+      build(model, view, indexPath)
+    }
+  }
+
+  public init(
+    register: Registrar,
+    build: Builder
+  ) {
+    self.register = register
+    self.build = build
+  }
+
+  public var register: Registrar
+  public var build: Builder
+}
+
+extension CellFactory {
+  public static func combined(_ factories: CellFactory...) -> CellFactory {
+    combined(factories)
+  }
+
+  public static func combined(_ factories: [CellFactory]) -> CellFactory {
+    CellFactory(
+      register: .init { collectionView in
+        factories.forEach { $0.register(in: collectionView) }
+      },
+      build: .init { model, collectionView, indexPath in
+        for factory in factories {
+          if let cell = factory.build(for: model, in: collectionView, at: indexPath) {
+            return cell
+          }
+        }
+        return nil
+      }
+    )
+  }
+}
+
+#if DEBUG
+extension CellFactory {
+  public static func unimplemented() -> CellFactory {
+    CellFactory(
+      register: .init(register: XCTUnimplemented("\(Self.self).Registrar")),
+      build: .init(build: XCTUnimplemented("\(Self.self).Builder"))
+    )
+  }
+}
+#endif
diff --git a/Sources/CollectionView/ViewConfigurator.swift b/Sources/CollectionView/ViewConfigurator.swift
new file mode 100644
index 0000000000000000000000000000000000000000..631f9e8d216b0a4ea6da2ce40db4fe48d7f103ef
--- /dev/null
+++ b/Sources/CollectionView/ViewConfigurator.swift
@@ -0,0 +1,22 @@
+import UIKit
+import XCTestDynamicOverlay
+
+public struct ViewConfigurator<View: UIView, Model> {
+  public init(configure: @escaping (View, Model) -> Void) {
+    self.configure = configure
+  }
+
+  public var configure: (View, Model) -> Void
+
+  public func callAsFunction(_ view: View, with model: Model) {
+    configure(view, model)
+  }
+}
+
+#if DEBUG
+extension ViewConfigurator {
+  public static func unimplemented() -> ViewConfigurator {
+    ViewConfigurator(configure: XCTUnimplemented("\(Self.self)"))
+  }
+}
+#endif
diff --git a/Tests/CollectionViewTests/CellFactoryTests.swift b/Tests/CollectionViewTests/CellFactoryTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..0fc5063570ee1d2f615d24e5cc2d38292e0ab003
--- /dev/null
+++ b/Tests/CollectionViewTests/CellFactoryTests.swift
@@ -0,0 +1,105 @@
+import CustomDump
+import XCTest
+@testable import CollectionView
+
+final class CellFactoryTests: XCTestCase {
+  func testCombined() {
+    struct Cell: Equatable {
+      var model: Int
+      var collectionView: UICollectionView
+      var indexPath: IndexPath
+    }
+
+    var didRegisterFirst = [UICollectionView]()
+    var didRegisterSecond = [UICollectionView]()
+    var didRegisterThird = [UICollectionView]()
+
+    var didBuildFirst = [Cell]()
+    var didBuildSecond = [Cell]()
+    var didBuildThird = [Cell]()
+
+    let factory = CellFactory<Int>.combined(
+      .init(
+        register: .init { didRegisterFirst.append($0) },
+        build: .init { model, collectionView, indexPath in
+          guard model == 1 else { return nil }
+          didBuildFirst.append(Cell(model: model, collectionView: collectionView, indexPath: indexPath))
+          return UICollectionViewCell()
+        }
+      ),
+      .init(
+        register: .init { didRegisterSecond.append($0) },
+        build: .init { model, collectionView, indexPath in
+          guard model == 2 else { return nil }
+          didBuildSecond.append(Cell(model: model, collectionView: collectionView, indexPath: indexPath))
+          return UICollectionViewCell()
+        }
+      ),
+      .init(
+        register: .init { didRegisterThird.append($0) },
+        build: .init { model, collectionView, indexPath in
+          guard model == 3 else { return nil }
+          didBuildThird.append(Cell(model: model, collectionView: collectionView, indexPath: indexPath))
+          return UICollectionViewCell()
+        }
+      )
+    )
+
+    let collectionView = UICollectionView(frame: .zero, collectionViewLayout: .init())
+
+    factory.register(in: collectionView)
+
+    XCTAssertEqual(didRegisterFirst, [collectionView])
+    XCTAssertEqual(didRegisterSecond, [collectionView])
+    XCTAssertEqual(didRegisterThird, [collectionView])
+
+    let firstCell = factory.build(for: 1, in: collectionView, at: IndexPath(item: 0, section: 1))
+
+    XCTAssertNotNil(firstCell)
+    XCTAssertNoDifference(didBuildFirst, [Cell(
+      model: 1,
+      collectionView: collectionView,
+      indexPath: IndexPath(row: 0, section: 1)
+    )])
+    XCTAssertNoDifference(didBuildSecond, [])
+    XCTAssertNoDifference(didBuildThird, [])
+
+    didBuildFirst = []
+    didBuildSecond = []
+    didBuildThird = []
+    let secondCell = factory.build(for: 2, in: collectionView, at: IndexPath(item: 2, section: 3))
+
+    XCTAssertNotNil(secondCell)
+    XCTAssertNoDifference(didBuildFirst, [])
+    XCTAssertNoDifference(didBuildSecond, [Cell(
+      model: 2,
+      collectionView: collectionView,
+      indexPath: IndexPath(row: 2, section: 3)
+    )])
+    XCTAssertNoDifference(didBuildThird, [])
+
+    didBuildFirst = []
+    didBuildSecond = []
+    didBuildThird = []
+    let thirdCell = factory.build(for: 3, in: collectionView, at: IndexPath(item: 4, section: 5))
+
+    XCTAssertNotNil(thirdCell)
+    XCTAssertNoDifference(didBuildFirst, [])
+    XCTAssertNoDifference(didBuildSecond, [])
+    XCTAssertNoDifference(didBuildThird, [Cell(
+      model: 3,
+      collectionView: collectionView,
+      indexPath: IndexPath(row: 4, section: 5)
+    )])
+
+    didBuildFirst = []
+    didBuildSecond = []
+    didBuildThird = []
+    let otherCell = factory.build(for: 4, in: collectionView, at: IndexPath(item: 0, section: 0))
+
+    XCTAssertNil(otherCell)
+    XCTAssertNoDifference(didBuildFirst, [])
+    XCTAssertNoDifference(didBuildSecond, [])
+    XCTAssertNoDifference(didBuildThird, [])
+  }
+}
diff --git a/Tests/CollectionViewTests/ViewConfiguratorTests.swift b/Tests/CollectionViewTests/ViewConfiguratorTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..df2e993b0f70bceefc124cca9fda9c3be5723db6
--- /dev/null
+++ b/Tests/CollectionViewTests/ViewConfiguratorTests.swift
@@ -0,0 +1,33 @@
+import CustomDump
+import XCTest
+@testable import CollectionView
+
+// MARK: - Example view configurator:
+
+private class ProfileView: UIView {
+  let username = UILabel()
+}
+
+private struct User {
+  var name: String
+}
+
+private extension ViewConfigurator where View == ProfileView, Model == User {
+  static let profileViewUserConfigurator = ViewConfigurator { view, model in
+    view.username.text = model.name
+  }
+}
+
+// MARK: - Tests:
+
+final class ViewConfiguratorTests: XCTestCase {
+  func testExampleConfigurator() {
+    let profileView = ProfileView()
+    let user = User(name: "John")
+
+    let configure = ViewConfigurator.profileViewUserConfigurator
+    configure(profileView, with: user)
+
+    XCTAssertNoDifference(profileView.username.text, user.name)
+  }
+}
diff --git a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 102be628eff74bf62db32030a74c7eed314ba3f3..f9de6930fb00ffc8a1befdc355e207f45741c671 100644
--- a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -328,8 +328,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/swift-custom-dump",
       "state" : {
-        "revision" : "51698ece74ecf31959d3fa81733f0a5363ef1b4e",
-        "version" : "0.3.0"
+        "revision" : "21ec1d717c07cea5a026979cb0471dd95c7087e7",
+        "version" : "0.5.0"
       }
     },
     {
@@ -373,8 +373,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
       "state" : {
-        "revision" : "50a70a9d3583fe228ce672e8923010c8df2deddd",
-        "version" : "0.2.1"
+        "revision" : "ef8e14e7ce1c0c304c644c6ba365d06c468ded6b",
+        "version" : "0.3.3"
       }
     }
   ],