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" } } ],