From 4a4f936350a7319a5b2f4332fd78a26c0f103cab Mon Sep 17 00:00:00 2001 From: Cal Stephens Date: Sun, 21 Dec 2025 08:25:28 -0800 Subject: [PATCH 1/3] Integrate Airbnb Swift Style Guide --- .github/workflows/swift.yml | 11 +++++++++-- Package.swift | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 58c9e70..62dc55a 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -12,10 +12,17 @@ jobs: strategy: matrix: xcode: - - '18.0' # Swift 6 + - '16.4' # Swift 6.1 steps: - uses: actions/checkout@v4 - name: Build run: xcodebuild clean build -scheme MagazineLayout -destination "generic/platform=iOS Simulator" - name: Run tests - run: xcodebuild clean test -project MagazineLayout.xcodeproj -scheme MagazineLayout -destination "name=iPhone 16,OS=18.4" + run: xcodebuild clean test -project MagazineLayout.xcodeproj -scheme MagazineLayout -destination "name=iPhone 16" + + lint-swift: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Lint Swift + run: swift package --allow-writing-to-package-directory format --lint diff --git a/Package.swift b/Package.swift index 882fa6b..1744c17 100644 --- a/Package.swift +++ b/Package.swift @@ -11,6 +11,9 @@ let package = Package( products: [ .library(name: "MagazineLayout", targets: ["MagazineLayout"]) ], + dependencies: [ + .package(url: "https://github.com/airbnb/swift", .upToNextMajor(from: "1.2.0")) + ], targets: [ .target( name: "MagazineLayout", From d37cd24f2a385e48274bb1aca5508d166a0a7d6c Mon Sep 17 00:00:00 2001 From: Cal Stephens Date: Sun, 21 Dec 2025 08:26:14 -0800 Subject: [PATCH 2/3] Run formatter --- .../MagazineLayoutExample/AppDelegate.swift | 31 +- .../GridDemoViewController.swift | 121 ++-- .../ListDemoViewController.swift | 184 +++--- .../MessageThreadDemoViewController.swift | 121 ++-- .../PerformanceDemoViewController.swift | 116 ++-- .../RootMenuViewController.swift | 94 ++- .../MagazineLayoutExample/SceneDelegate.swift | 18 +- MagazineLayout/LayoutCore/LayoutState.swift | 21 +- MagazineLayout/LayoutCore/ModelState.swift | 232 ++++--- MagazineLayout/LayoutCore/SectionModel.swift | 119 ++-- .../LayoutCore/Types/ElementLocation.swift | 6 +- .../Types/ElementLocationFramePairs.swift | 10 +- ...zineLayoutItemWidthMode+WidthDivisor.swift | 2 +- .../Types/MagazineLayoutSectionMetrics.swift | 44 +- .../UITraitCollection+DisplayScale.swift | 6 +- MagazineLayout/Public/MagazineLayout.swift | 501 +++++++++------- ...LayoutCollectionViewLayoutAttributes.swift | 2 +- .../Public/Types/MagazineLayout+Default.swift | 7 +- .../MagazineLayoutFooterVisibilityMode.swift | 7 +- .../MagazineLayoutHeaderVisibilityMode.swift | 7 +- .../Types/MagazineLayoutItemSizeMode.swift | 12 +- ...CollectionViewDelegateMagazineLayout.swift | 172 +++--- ...MagazineLayoutCollectionReusableView.swift | 8 +- .../MagazineLayoutCollectionViewCell.swift | 8 +- Package.swift | 46 +- ...tentInsetAdjustingContentOffsetTests.swift | 21 +- Tests/ElementLocationFramePairsTests.swift | 90 ++- .../LayoutStateTargetContentOffsetTests.swift | 124 ++-- Tests/ModelStateEmptySectionLayoutTests.swift | 69 ++- Tests/ModelStateInitiallSetUpTests.swift | 33 +- Tests/ModelStateLayoutTests.swift | 567 ++++++++++-------- Tests/ModelStateUpdateTests.swift | 275 +++++---- Tests/RowOffsetTrackerTests.swift | 146 ++++- Tests/ScreenPixelAlignmentTests.swift | 32 +- Tests/TestingSupport.swift | 53 +- 35 files changed, 1939 insertions(+), 1366 deletions(-) diff --git a/Example/MagazineLayoutExample/AppDelegate.swift b/Example/MagazineLayoutExample/AppDelegate.swift index 1458ba8..bd516d3 100644 --- a/Example/MagazineLayoutExample/AppDelegate.swift +++ b/Example/MagazineLayoutExample/AppDelegate.swift @@ -18,48 +18,45 @@ import UIKit @UIApplicationMain final class AppDelegate: UIResponder, UIApplicationDelegate { - // MARK: Internal - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) - -> Bool - { - return true + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + true } func application( - _ application: UIApplication, + _: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions) - -> UISceneConfiguration - { + options _: UIScene.ConnectionOptions + ) -> UISceneConfiguration { let sceneConfiguration = UISceneConfiguration( name: "Default Configuration", - sessionRole: connectingSceneSession.role) + sessionRole: connectingSceneSession.role + ) sceneConfiguration.delegateClass = SceneDelegate.self return sceneConfiguration } - func applicationWillResignActive(_ application: UIApplication) { + func applicationWillResignActive(_: UIApplication) { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. } - func applicationDidEnterBackground(_ application: UIApplication) { + func applicationDidEnterBackground(_: UIApplication) { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. } - func applicationWillEnterForeground(_ application: UIApplication) { + func applicationWillEnterForeground(_: UIApplication) { // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. } - func applicationDidBecomeActive(_ application: UIApplication) { + func applicationDidBecomeActive(_: UIApplication) { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } - func applicationWillTerminate(_ application: UIApplication) { + func applicationWillTerminate(_: UIApplication) { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } diff --git a/Example/MagazineLayoutExample/GridDemoViewController.swift b/Example/MagazineLayoutExample/GridDemoViewController.swift index 51cbc37..bf18571 100644 --- a/Example/MagazineLayoutExample/GridDemoViewController.swift +++ b/Example/MagazineLayoutExample/GridDemoViewController.swift @@ -33,12 +33,14 @@ final class GridDemoViewController: UIViewController { UIBarButtonItem( barButtonSystemItem: .add, target: self, - action: #selector(addButtonTapped)), + action: #selector(addButtonTapped) + ), UIBarButtonItem( image: UIImage(systemName: "shuffle"), style: .plain, target: self, - action: #selector(shuffleButtonTapped)), + action: #selector(shuffleButtonTapped) + ), ] view.addSubview(collectionView) @@ -67,8 +69,7 @@ final class GridDemoViewController: UIViewController { }() private lazy var dataSource: DataSource = { - let cellRegistration = UICollectionView.CellRegistration - { cell, indexPath, item in + let cellRegistration = UICollectionView.CellRegistration { cell, _, item in cell.contentConfiguration = UIHostingConfiguration { GridItemView(item: item) } @@ -81,11 +82,13 @@ final class GridDemoViewController: UIViewController { collectionView.dequeueConfiguredReusableCell( using: cellRegistration, for: indexPath, - item: item) - }) + item: item + ) + } + ) }() - private var items: [GridItem] = [] + private var items = [GridItem]() private func loadInitialData() { let widthModes: [MagazineLayoutItemWidthMode] = [ @@ -113,7 +116,8 @@ final class GridDemoViewController: UIViewController { GridItem( text: textForWidthMode(widthMode), color: colorForWidthMode(widthMode), - widthMode: widthMode) + widthMode: widthMode + ) } applySnapshot(animatingDifferences: false) @@ -130,11 +134,14 @@ final class GridDemoViewController: UIViewController { switch widthMode { case .fullWidth: return .systemRed + case .halfWidth: return .systemBlue + case .thirdWidth: return .systemGreen - case let .fractionalWidth(divisor): + + case .fractionalWidth(let divisor): if divisor == 4 { return .systemPurple } else if divisor == 5 { @@ -144,6 +151,7 @@ final class GridDemoViewController: UIViewController { } else { return .systemTeal } + @unknown default: return .systemRed } @@ -153,11 +161,14 @@ final class GridDemoViewController: UIViewController { switch widthMode { case .fullWidth: return "Full Width" + case .halfWidth: return "Half Width" + case .thirdWidth: return "Third Width" - case let .fractionalWidth(divisor): + + case .fractionalWidth(let divisor): if divisor == 4 { return "Quarter Width" } else if divisor == 5 { @@ -165,6 +176,7 @@ final class GridDemoViewController: UIViewController { } else { return "1/\(divisor) Width" } + @unknown default: return "Unknown Width" } @@ -185,7 +197,8 @@ final class GridDemoViewController: UIViewController { let newItem = GridItem( text: textForWidthMode(selectedWidthMode), color: colorForWidthMode(selectedWidthMode), - widthMode: selectedWidthMode) + widthMode: selectedWidthMode + ) let insertIndex = Int.random(in: 0...items.count) items.insert(newItem, at: insertIndex) @@ -203,7 +216,7 @@ final class GridDemoViewController: UIViewController { // MARK: UICollectionViewDelegate extension GridDemoViewController: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { items.remove(at: indexPath.item) applySnapshot() } @@ -213,76 +226,69 @@ extension GridDemoViewController: UICollectionViewDelegate { extension GridDemoViewController: UICollectionViewDelegateMagazineLayout { func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - sizeModeForItemAt indexPath: IndexPath) - -> MagazineLayoutItemSizeMode - { - return MagazineLayoutItemSizeMode( + _: UICollectionView, + layout _: UICollectionViewLayout, + sizeModeForItemAt indexPath: IndexPath + ) -> MagazineLayoutItemSizeMode { + MagazineLayoutItemSizeMode( widthMode: items[indexPath.item].widthMode, - heightMode: .dynamic) + heightMode: .dynamic + ) } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForHeaderInSectionAtIndex index: Int) - -> MagazineLayoutHeaderVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForHeaderInSectionAtIndex _: Int + ) -> MagazineLayoutHeaderVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForFooterInSectionAtIndex index: Int) - -> MagazineLayoutFooterVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForFooterInSectionAtIndex _: Int + ) -> MagazineLayoutFooterVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForBackgroundInSectionAtIndex index: Int) - -> MagazineLayoutBackgroundVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForBackgroundInSectionAtIndex _: Int + ) -> MagazineLayoutBackgroundVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - horizontalSpacingForItemsInSectionAtIndex index: Int) - -> CGFloat - { + _: UICollectionView, + layout _: UICollectionViewLayout, + horizontalSpacingForItemsInSectionAtIndex _: Int + ) -> CGFloat { 12 } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - verticalSpacingForElementsInSectionAtIndex index: Int) - -> CGFloat - { + _: UICollectionView, + layout _: UICollectionViewLayout, + verticalSpacingForElementsInSectionAtIndex _: Int + ) -> CGFloat { 12 } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetsForSectionAtIndex index: Int) - -> UIEdgeInsets - { + _: UICollectionView, + layout _: UICollectionViewLayout, + insetsForSectionAtIndex _: Int + ) -> UIEdgeInsets { UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetsForItemsInSectionAtIndex index: Int) - -> UIEdgeInsets - { + _: UICollectionView, + layout _: UICollectionViewLayout, + insetsForItemsInSectionAtIndex _: Int + ) -> UIEdgeInsets { .zero } } @@ -296,13 +302,14 @@ private struct GridItem: Hashable { let color: UIColor let widthMode: MagazineLayoutItemWidthMode + static func ==(lhs: GridItem, rhs: GridItem) -> Bool { + lhs.id == rhs.id + } + func hash(into hasher: inout Hasher) { hasher.combine(id) } - static func == (lhs: GridItem, rhs: GridItem) -> Bool { - lhs.id == rhs.id - } } // MARK: - GridItemView diff --git a/Example/MagazineLayoutExample/ListDemoViewController.swift b/Example/MagazineLayoutExample/ListDemoViewController.swift index 62c6552..be1a198 100644 --- a/Example/MagazineLayoutExample/ListDemoViewController.swift +++ b/Example/MagazineLayoutExample/ListDemoViewController.swift @@ -33,12 +33,14 @@ final class ListDemoViewController: UIViewController { UIBarButtonItem( barButtonSystemItem: .add, target: self, - action: #selector(addButtonTapped)), + action: #selector(addButtonTapped) + ), UIBarButtonItem( image: UIImage(systemName: "shuffle"), style: .plain, target: self, - action: #selector(shuffleButtonTapped)), + action: #selector(shuffleButtonTapped) + ), ] view.addSubview(collectionView) @@ -67,8 +69,7 @@ final class ListDemoViewController: UIViewController { }() private lazy var dataSource: DataSource = { - let cellRegistration = UICollectionView.CellRegistration - { cell, indexPath, item in + let cellRegistration = UICollectionView.CellRegistration { cell, _, item in cell.contentConfiguration = UIHostingConfiguration { ListItemView(item: item) } @@ -77,7 +78,7 @@ final class ListDemoViewController: UIViewController { let headerRegistration = UICollectionView.SupplementaryRegistration( elementKind: MagazineLayout.SupplementaryViewKind.sectionHeader - ) { [weak self] supplementaryView, elementKind, indexPath in + ) { [weak self] supplementaryView, _, indexPath in guard let self, indexPath.section < self.sections.count else { return } let section = self.sections[indexPath.section] @@ -89,7 +90,7 @@ final class ListDemoViewController: UIViewController { let footerRegistration = UICollectionView.SupplementaryRegistration( elementKind: MagazineLayout.SupplementaryViewKind.sectionFooter - ) { supplementaryView, elementKind, indexPath in + ) { supplementaryView, _, _ in supplementaryView.contentConfiguration = UIHostingConfiguration { SectionFooterView() } @@ -102,19 +103,25 @@ final class ListDemoViewController: UIViewController { collectionView.dequeueConfiguredReusableCell( using: cellRegistration, for: indexPath, - item: item) - }) + item: item + ) + } + ) dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in switch kind { case MagazineLayout.SupplementaryViewKind.sectionHeader: return collectionView.dequeueConfiguredReusableSupplementary( using: headerRegistration, - for: indexPath) + for: indexPath + ) + case MagazineLayout.SupplementaryViewKind.sectionFooter: return collectionView.dequeueConfiguredReusableSupplementary( using: footerRegistration, - for: indexPath) + for: indexPath + ) + default: return nil } @@ -123,7 +130,7 @@ final class ListDemoViewController: UIViewController { return dataSource }() - private var sections: [ListSection] = [] + private var sections = [ListSection]() private func loadInitialData() { sections = [ @@ -133,54 +140,66 @@ final class ListDemoViewController: UIViewController { ListItem( title: "Item 1", subtitle: "A featured item with important content", - color: .systemRed), + color: .systemRed + ), ListItem( title: "Item 2", subtitle: "Another featured item to showcase", - color: .systemBlue), + color: .systemBlue + ), ListItem( title: "Item 3", subtitle: "The third item in this section", - color: .systemGreen), + color: .systemGreen + ), ], headerPinned: true, - footerPinned: false), + footerPinned: false + ), ListSection( title: "Regular Items", items: [ ListItem( title: "Item A", subtitle: "A regular item in the list", - color: .systemPurple), + color: .systemPurple + ), ListItem( title: "Item B", subtitle: "Another regular item", - color: .systemCyan), + color: .systemCyan + ), ListItem( title: "Item C", subtitle: "Yet another item", - color: .systemOrange), + color: .systemOrange + ), ListItem( title: "Item D", subtitle: "More content here", - color: .systemTeal), + color: .systemTeal + ), ], headerPinned: true, - footerPinned: false), + footerPinned: false + ), ListSection( title: "Special Section", items: [ ListItem( title: "Special 1", subtitle: "This section has a pinned footer", - color: .systemPink), + color: .systemPink + ), ListItem( title: "Special 2", subtitle: "Notice the footer sticks", - color: .systemYellow), + color: .systemYellow + ), ], headerPinned: true, - footerPinned: true), + footerPinned: true + ), ] applySnapshot(animatingDifferences: false) @@ -201,13 +220,22 @@ final class ListDemoViewController: UIViewController { let randomSectionIndex = Int.random(in: 0.. MagazineLayoutItemSizeMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + sizeModeForItemAt _: IndexPath + ) -> MagazineLayoutItemSizeMode { MagazineLayoutItemSizeMode( widthMode: .fullWidth(respectsHorizontalInsets: true), - heightMode: .dynamic) + heightMode: .dynamic + ) } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForHeaderInSectionAtIndex index: Int) - -> MagazineLayoutHeaderVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForHeaderInSectionAtIndex index: Int + ) -> MagazineLayoutHeaderVisibilityMode { let section = sections[index] return .visible(heightMode: .dynamic, pinToVisibleBounds: section.headerPinned) } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForFooterInSectionAtIndex index: Int) - -> MagazineLayoutFooterVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForFooterInSectionAtIndex index: Int + ) -> MagazineLayoutFooterVisibilityMode { let section = sections[index] return .visible(heightMode: .dynamic, pinToVisibleBounds: section.footerPinned) } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForBackgroundInSectionAtIndex index: Int) - -> MagazineLayoutBackgroundVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForBackgroundInSectionAtIndex _: Int + ) -> MagazineLayoutBackgroundVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - horizontalSpacingForItemsInSectionAtIndex index: Int) - -> CGFloat - { + _: UICollectionView, + layout _: UICollectionViewLayout, + horizontalSpacingForItemsInSectionAtIndex _: Int + ) -> CGFloat { 12 } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - verticalSpacingForElementsInSectionAtIndex index: Int) - -> CGFloat - { + _: UICollectionView, + layout _: UICollectionViewLayout, + verticalSpacingForElementsInSectionAtIndex _: Int + ) -> CGFloat { 12 } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetsForSectionAtIndex index: Int) - -> UIEdgeInsets - { + _: UICollectionView, + layout _: UICollectionViewLayout, + insetsForSectionAtIndex _: Int + ) -> UIEdgeInsets { UIEdgeInsets(top: 0, left: 0, bottom: 24, right: 0) } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetsForItemsInSectionAtIndex index: Int) - -> UIEdgeInsets - { + _: UICollectionView, + layout _: UICollectionViewLayout, + insetsForItemsInSectionAtIndex _: Int + ) -> UIEdgeInsets { UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) } } @@ -353,8 +377,8 @@ private struct ListSection: Hashable { title: String, items: [ListItem], headerPinned: Bool = true, - footerPinned: Bool = false) - { + footerPinned: Bool = false + ) { self.id = id self.title = title self.items = items @@ -370,13 +394,14 @@ private struct ListSection: Hashable { let headerPinned: Bool let footerPinned: Bool + static func ==(lhs: ListSection, rhs: ListSection) -> Bool { + lhs.id == rhs.id + } + func hash(into hasher: inout Hasher) { hasher.combine(id) } - static func == (lhs: ListSection, rhs: ListSection) -> Bool { - lhs.id == rhs.id - } } // MARK: - ListItem @@ -388,13 +413,14 @@ private struct ListItem: Hashable { let subtitle: String let color: UIColor + static func ==(lhs: ListItem, rhs: ListItem) -> Bool { + lhs.id == rhs.id + } + func hash(into hasher: inout Hasher) { hasher.combine(id) } - static func == (lhs: ListItem, rhs: ListItem) -> Bool { - lhs.id == rhs.id - } } // MARK: - ListItemView diff --git a/Example/MagazineLayoutExample/MessageThreadDemoViewController.swift b/Example/MagazineLayoutExample/MessageThreadDemoViewController.swift index d4bd105..db5438c 100644 --- a/Example/MagazineLayoutExample/MessageThreadDemoViewController.swift +++ b/Example/MagazineLayoutExample/MessageThreadDemoViewController.swift @@ -34,12 +34,14 @@ final class MessageThreadDemoViewController: UIViewController { title: "Send", style: .plain, target: self, - action: #selector(sendButtonTapped)), + action: #selector(sendButtonTapped) + ), UIBarButtonItem( title: "Receive", style: .plain, target: self, - action: #selector(receiveButtonTapped)), + action: #selector(receiveButtonTapped) + ), ] view.addSubview(collectionView) @@ -69,8 +71,7 @@ final class MessageThreadDemoViewController: UIViewController { }() private lazy var dataSource: DataSource = { - let cellRegistration = UICollectionView.CellRegistration - { cell, indexPath, message in + let cellRegistration = UICollectionView.CellRegistration { cell, _, message in cell.contentConfiguration = UIHostingConfiguration { MessageView(message: message) } @@ -83,11 +84,13 @@ final class MessageThreadDemoViewController: UIViewController { collectionView.dequeueConfiguredReusableCell( using: cellRegistration, for: indexPath, - item: message) - }) + item: message + ) + } + ) }() - private var messages: [Message] = [] + private var messages = [Message]() private var messageCounter = 0 private var oldestMessageDate = Date() private var isLoadingMore = false @@ -120,7 +123,7 @@ final class MessageThreadDemoViewController: UIViewController { private func loadInitialMessages() { // Create 10 initial messages alternating between sent and received - var initialMessages: [Message] = [] + var initialMessages = [Message]() var timestamp = Date() for i in 0..<20 { @@ -132,7 +135,8 @@ final class MessageThreadDemoViewController: UIViewController { initialMessages.append(Message( text: text, isSent: isSent, - timestamp: timestamp)) + timestamp: timestamp + )) timestamp = timestamp.addingTimeInterval(-60) // 1 minute earlier } @@ -156,7 +160,7 @@ final class MessageThreadDemoViewController: UIViewController { isLoadingMore = true // Simulate loading 10 older messages - var olderMessages: [Message] = [] + var olderMessages = [Message]() var timestamp = oldestMessageDate.addingTimeInterval(-60) for i in 0..<10 { @@ -168,7 +172,8 @@ final class MessageThreadDemoViewController: UIViewController { olderMessages.append(Message( text: text, isSent: isSent, - timestamp: timestamp)) + timestamp: timestamp + )) timestamp = timestamp.addingTimeInterval(-60) } @@ -186,7 +191,8 @@ final class MessageThreadDemoViewController: UIViewController { private func sendButtonTapped() { let newMessage = Message( text: sentMessages.randomElement() ?? "Hello!", - isSent: true) + isSent: true + ) messages.append(newMessage) messageCounter += 1 @@ -198,7 +204,8 @@ final class MessageThreadDemoViewController: UIViewController { private func receiveButtonTapped() { let newMessage = Message( text: receivedMessages.randomElement() ?? "Hi there!", - isSent: false) + isSent: false + ) messages.append(newMessage) messageCounter += 1 @@ -211,7 +218,7 @@ final class MessageThreadDemoViewController: UIViewController { extension MessageThreadDemoViewController: UICollectionViewDelegate { func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - if scrollView.contentOffset.y <= -scrollView.adjustedContentInset.top && !isLoadingMore { + if scrollView.contentOffset.y <= -scrollView.adjustedContentInset.top, !isLoadingMore { loadOlderMessages() } } @@ -221,76 +228,69 @@ extension MessageThreadDemoViewController: UICollectionViewDelegate { extension MessageThreadDemoViewController: UICollectionViewDelegateMagazineLayout { func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - sizeModeForItemAt indexPath: IndexPath) - -> MagazineLayoutItemSizeMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + sizeModeForItemAt _: IndexPath + ) -> MagazineLayoutItemSizeMode { MagazineLayoutItemSizeMode( widthMode: .fullWidth(respectsHorizontalInsets: true), - heightMode: .dynamic) + heightMode: .dynamic + ) } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForHeaderInSectionAtIndex index: Int) - -> MagazineLayoutHeaderVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForHeaderInSectionAtIndex _: Int + ) -> MagazineLayoutHeaderVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForFooterInSectionAtIndex index: Int) - -> MagazineLayoutFooterVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForFooterInSectionAtIndex _: Int + ) -> MagazineLayoutFooterVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForBackgroundInSectionAtIndex index: Int) - -> MagazineLayoutBackgroundVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForBackgroundInSectionAtIndex _: Int + ) -> MagazineLayoutBackgroundVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - horizontalSpacingForItemsInSectionAtIndex index: Int) - -> CGFloat - { + _: UICollectionView, + layout _: UICollectionViewLayout, + horizontalSpacingForItemsInSectionAtIndex _: Int + ) -> CGFloat { 12 } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - verticalSpacingForElementsInSectionAtIndex index: Int) - -> CGFloat - { + _: UICollectionView, + layout _: UICollectionViewLayout, + verticalSpacingForElementsInSectionAtIndex _: Int + ) -> CGFloat { 8 } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetsForSectionAtIndex index: Int) - -> UIEdgeInsets - { + _: UICollectionView, + layout _: UICollectionViewLayout, + insetsForSectionAtIndex _: Int + ) -> UIEdgeInsets { UIEdgeInsets(top: 16, left: 0, bottom: 16, right: 0) } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetsForItemsInSectionAtIndex index: Int) - -> UIEdgeInsets - { + _: UICollectionView, + layout _: UICollectionViewLayout, + insetsForItemsInSectionAtIndex _: Int + ) -> UIEdgeInsets { UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) } } @@ -304,8 +304,8 @@ private struct Message: Hashable { init( text: String, isSent: Bool, - timestamp: Date = Date()) - { + timestamp: Date = Date() + ) { self.text = text self.isSent = isSent self.timestamp = timestamp @@ -318,13 +318,14 @@ private struct Message: Hashable { let isSent: Bool let timestamp: Date + static func ==(lhs: Message, rhs: Message) -> Bool { + lhs.id == rhs.id + } + func hash(into hasher: inout Hasher) { hasher.combine(id) } - static func == (lhs: Message, rhs: Message) -> Bool { - lhs.id == rhs.id - } } // MARK: - MessageView diff --git a/Example/MagazineLayoutExample/PerformanceDemoViewController.swift b/Example/MagazineLayoutExample/PerformanceDemoViewController.swift index cd3b0ad..6d3ea4a 100644 --- a/Example/MagazineLayoutExample/PerformanceDemoViewController.swift +++ b/Example/MagazineLayoutExample/PerformanceDemoViewController.swift @@ -33,7 +33,8 @@ final class PerformanceDemoViewController: UIViewController { UIBarButtonItem( barButtonSystemItem: .add, target: self, - action: #selector(addButtonTapped)), + action: #selector(addButtonTapped) + ) ] view.addSubview(collectionView) @@ -58,16 +59,25 @@ final class PerformanceDemoViewController: UIViewController { collectionView.delegate = self collectionView.register( MagazineLayoutCollectionViewCell.self, - forCellWithReuseIdentifier: "PerformanceCell") + forCellWithReuseIdentifier: "PerformanceCell" + ) return collectionView }() - private var items: [PerformanceItem] = [] + private var items = [PerformanceItem]() private var nextItemID = 0 private let colors: [UIColor] = [ - .systemRed, .systemOrange, .systemYellow, .systemGreen, .systemTeal, - .systemBlue, .systemIndigo, .systemPurple, .systemPink, .systemCyan + .systemRed, + .systemOrange, + .systemYellow, + .systemGreen, + .systemTeal, + .systemBlue, + .systemIndigo, + .systemPurple, + .systemPink, + .systemCyan, ] private func loadInitialData() { @@ -86,7 +96,8 @@ final class PerformanceDemoViewController: UIViewController { private func addButtonTapped() { let newItem = PerformanceItem( id: nextItemID, - color: colors.randomElement() ?? .systemBlue) + color: colors.randomElement() ?? .systemBlue + ) nextItemID += 1 // Insert at index 0 with manual batch update @@ -100,26 +111,25 @@ final class PerformanceDemoViewController: UIViewController { // MARK: UICollectionViewDataSource extension PerformanceDemoViewController: UICollectionViewDataSource { - func numberOfSections(in collectionView: UICollectionView) -> Int { - return 1 + func numberOfSections(in _: UICollectionView) -> Int { + 1 } func collectionView( - _ collectionView: UICollectionView, - numberOfItemsInSection section: Int) - -> Int - { - return items.count + _: UICollectionView, + numberOfItemsInSection _: Int + ) -> Int { + items.count } func collectionView( _ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath) - -> UICollectionViewCell - { + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell( withReuseIdentifier: "PerformanceCell", - for: indexPath) + for: indexPath + ) let item = items[indexPath.item] @@ -150,74 +160,66 @@ extension PerformanceDemoViewController: UICollectionViewDelegate { extension PerformanceDemoViewController: UICollectionViewDelegateMagazineLayout { func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - sizeModeForItemAt indexPath: IndexPath) - -> MagazineLayoutItemSizeMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + sizeModeForItemAt _: IndexPath + ) -> MagazineLayoutItemSizeMode { MagazineLayoutItemSizeMode(widthMode: .halfWidth, heightMode: .dynamic) } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForHeaderInSectionAtIndex index: Int) - -> MagazineLayoutHeaderVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForHeaderInSectionAtIndex _: Int + ) -> MagazineLayoutHeaderVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForFooterInSectionAtIndex index: Int) - -> MagazineLayoutFooterVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForFooterInSectionAtIndex _: Int + ) -> MagazineLayoutFooterVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForBackgroundInSectionAtIndex index: Int) - -> MagazineLayoutBackgroundVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForBackgroundInSectionAtIndex _: Int + ) -> MagazineLayoutBackgroundVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - horizontalSpacingForItemsInSectionAtIndex index: Int) - -> CGFloat - { + _: UICollectionView, + layout _: UICollectionViewLayout, + horizontalSpacingForItemsInSectionAtIndex _: Int + ) -> CGFloat { 12 } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - verticalSpacingForElementsInSectionAtIndex index: Int) - -> CGFloat - { + _: UICollectionView, + layout _: UICollectionViewLayout, + verticalSpacingForElementsInSectionAtIndex _: Int + ) -> CGFloat { 12 } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetsForSectionAtIndex index: Int) - -> UIEdgeInsets - { + _: UICollectionView, + layout _: UICollectionViewLayout, + insetsForSectionAtIndex _: Int + ) -> UIEdgeInsets { UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetsForItemsInSectionAtIndex index: Int) - -> UIEdgeInsets - { + _: UICollectionView, + layout _: UICollectionViewLayout, + insetsForItemsInSectionAtIndex _: Int + ) -> UIEdgeInsets { .zero } } diff --git a/Example/MagazineLayoutExample/RootMenuViewController.swift b/Example/MagazineLayoutExample/RootMenuViewController.swift index 7b5cf31..039fc01 100644 --- a/Example/MagazineLayoutExample/RootMenuViewController.swift +++ b/Example/MagazineLayoutExample/RootMenuViewController.swift @@ -43,6 +43,9 @@ final class RootMenuViewController: UIViewController { // MARK: Private + private typealias DataSource = UICollectionViewDiffableDataSource + private typealias Snapshot = NSDiffableDataSourceSnapshot + private lazy var collectionView: UICollectionView = { let layout = MagazineLayout() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) @@ -51,12 +54,8 @@ final class RootMenuViewController: UIViewController { return collectionView }() - private typealias DataSource = UICollectionViewDiffableDataSource - private typealias Snapshot = NSDiffableDataSourceSnapshot - private lazy var dataSource: DataSource = { - let cellRegistration = UICollectionView.CellRegistration - { cell, indexPath, option in + let cellRegistration = UICollectionView.CellRegistration { cell, _, option in cell.contentConfiguration = UIHostingConfiguration { MenuItemView(option: option) } @@ -69,8 +68,10 @@ final class RootMenuViewController: UIViewController { collectionView.dequeueConfiguredReusableCell( using: cellRegistration, for: indexPath, - item: option) - }) + item: option + ) + } + ) }() private func applyInitialSnapshot() { @@ -84,7 +85,7 @@ final class RootMenuViewController: UIViewController { // MARK: UICollectionViewDelegate extension RootMenuViewController: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let option = dataSource.itemIdentifier(for: indexPath) else { return } let viewController: UIViewController @@ -107,76 +108,69 @@ extension RootMenuViewController: UICollectionViewDelegate { extension RootMenuViewController: UICollectionViewDelegateMagazineLayout { func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - sizeModeForItemAt indexPath: IndexPath) - -> MagazineLayoutItemSizeMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + sizeModeForItemAt _: IndexPath + ) -> MagazineLayoutItemSizeMode { MagazineLayoutItemSizeMode( widthMode: .fullWidth(respectsHorizontalInsets: true), - heightMode: .dynamic) + heightMode: .dynamic + ) } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForHeaderInSectionAtIndex index: Int) - -> MagazineLayoutHeaderVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForHeaderInSectionAtIndex _: Int + ) -> MagazineLayoutHeaderVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForFooterInSectionAtIndex index: Int) - -> MagazineLayoutFooterVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForFooterInSectionAtIndex _: Int + ) -> MagazineLayoutFooterVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForBackgroundInSectionAtIndex index: Int) - -> MagazineLayoutBackgroundVisibilityMode - { + _: UICollectionView, + layout _: UICollectionViewLayout, + visibilityModeForBackgroundInSectionAtIndex _: Int + ) -> MagazineLayoutBackgroundVisibilityMode { .hidden } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - horizontalSpacingForItemsInSectionAtIndex index: Int) - -> CGFloat - { + _: UICollectionView, + layout _: UICollectionViewLayout, + horizontalSpacingForItemsInSectionAtIndex _: Int + ) -> CGFloat { 16 } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - verticalSpacingForElementsInSectionAtIndex index: Int) - -> CGFloat - { + _: UICollectionView, + layout _: UICollectionViewLayout, + verticalSpacingForElementsInSectionAtIndex _: Int + ) -> CGFloat { 16 } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetsForSectionAtIndex index: Int) - -> UIEdgeInsets - { + _: UICollectionView, + layout _: UICollectionViewLayout, + insetsForSectionAtIndex _: Int + ) -> UIEdgeInsets { UIEdgeInsets(top: 16, left: 0, bottom: 16, right: 0) } func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetsForItemsInSectionAtIndex index: Int) - -> UIEdgeInsets - { + _: UICollectionView, + layout _: UICollectionViewLayout, + insetsForItemsInSectionAtIndex _: Int + ) -> UIEdgeInsets { UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) } } @@ -189,6 +183,8 @@ private enum DemoOption: String, CaseIterable { case messageThread = "Message Thread" case performance = "Performance" + // MARK: Internal + var subtitle: String { switch self { case .grid: diff --git a/Example/MagazineLayoutExample/SceneDelegate.swift b/Example/MagazineLayoutExample/SceneDelegate.swift index b357d7d..7880f8a 100644 --- a/Example/MagazineLayoutExample/SceneDelegate.swift +++ b/Example/MagazineLayoutExample/SceneDelegate.swift @@ -17,15 +17,13 @@ import UIKit final class SceneDelegate: UIResponder, UIWindowSceneDelegate { - // MARK: Internal - var window: UIWindow? func scene( _ scene: UIScene, - willConnectTo session: UISceneSession, - options connectionOptions: UIScene.ConnectionOptions) - { + willConnectTo _: UISceneSession, + options _: UIScene.ConnectionOptions + ) { guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScene) @@ -38,23 +36,23 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.makeKeyAndVisible() } - func sceneDidDisconnect(_ scene: UIScene) { + func sceneDidDisconnect(_: UIScene) { // Called as the scene is being released by the system. } - func sceneDidBecomeActive(_ scene: UIScene) { + func sceneDidBecomeActive(_: UIScene) { // Called when the scene has moved from an inactive state to an active state. } - func sceneWillResignActive(_ scene: UIScene) { + func sceneWillResignActive(_: UIScene) { // Called when the scene will move from an active state to an inactive state. } - func sceneWillEnterForeground(_ scene: UIScene) { + func sceneWillEnterForeground(_: UIScene) { // Called as the scene transitions from the background to the foreground. } - func sceneDidEnterBackground(_ scene: UIScene) { + func sceneDidEnterBackground(_: UIScene) { // Called as the scene transitions from the foreground to the background. } } diff --git a/MagazineLayout/LayoutCore/LayoutState.swift b/MagazineLayout/LayoutCore/LayoutState.swift index 11ba80d..1526ba2 100644 --- a/MagazineLayout/LayoutCore/LayoutState.swift +++ b/MagazineLayout/LayoutCore/LayoutState.swift @@ -20,8 +20,6 @@ import UIKit /// Represents the state of the layout, including metrics and the current `ModelState`. struct LayoutState { - // MARK: Internal - let modelState: ModelState var bounds: CGRect @@ -84,9 +82,11 @@ struct LayoutState { let firstVisibleItemLocationFramePair, let lastVisibleItemLocationFramePair, let firstVisibleItemID = modelState.idForItemModel( - at: firstVisibleItemLocationFramePair.elementLocation.indexPath), + at: firstVisibleItemLocationFramePair.elementLocation.indexPath + ), let lastVisibleItemID = modelState.idForItemModel( - at: lastVisibleItemLocationFramePair.elementLocation.indexPath) + at: lastVisibleItemLocationFramePair.elementLocation.indexPath + ) else { switch verticalLayoutDirection { case .topToBottom: return .top @@ -125,8 +125,10 @@ struct LayoutState { return .topItem( id: firstVisibleItemID, elementLocation: firstVisibleItemLocationFramePair.elementLocation, - distanceFromTop: distanceFromTop.alignedToPixel(forScreenWithScale: scale)) + distanceFromTop: distanceFromTop.alignedToPixel(forScreenWithScale: scale) + ) } + case .bottomToTop: switch position { case .atTop, .inMiddle: @@ -135,7 +137,9 @@ struct LayoutState { return .bottomItem( id: lastVisibleItemID, elementLocation: lastVisibleItemLocationFramePair.elementLocation, - distanceFromBottom: distanceFromBottom.alignedToPixel(forScreenWithScale: scale)) + distanceFromBottom: distanceFromBottom.alignedToPixel(forScreenWithScale: scale) + ) + case .atBottom: return .bottom } @@ -144,9 +148,8 @@ struct LayoutState { func yOffset( for targetContentOffsetAnchor: TargetContentOffsetAnchor, - isPerformingBatchUpdates: Bool) - -> CGFloat - { + isPerformingBatchUpdates: Bool + ) -> CGFloat { switch targetContentOffsetAnchor { case .top: return minContentOffset.y diff --git a/MagazineLayout/LayoutCore/ModelState.swift b/MagazineLayout/LayoutCore/ModelState.swift index a3166fc..fae3fd9 100755 --- a/MagazineLayout/LayoutCore/ModelState.swift +++ b/MagazineLayout/LayoutCore/ModelState.swift @@ -43,8 +43,8 @@ final class ModelState { func idForItemModel(at indexPath: IndexPath) -> UInt64? { guard indexPath.section < sectionModels.count, - indexPath.item < sectionModels[indexPath.section].numberOfItems else - { + indexPath.item < sectionModels[indexPath.section].numberOfItems + else { // This occurs when getting layout attributes for initial / final animations return nil } @@ -86,7 +86,7 @@ final class ModelState { switch item.sizeMode.heightMode { case .static: return true - case .dynamicAndStretchToTallestItemInRow, .dynamic(_): + case .dynamicAndStretchToTallestItemInRow, .dynamic: return item.preferredHeight != nil } } @@ -94,8 +94,8 @@ final class ModelState { func itemModelHeightMode(at indexPath: IndexPath) -> MagazineLayoutItemHeightMode? { guard indexPath.section < sectionModels.count, - indexPath.item < sectionModels[indexPath.section].numberOfItems else - { + indexPath.item < sectionModels[indexPath.section].numberOfItems + else { assertionFailure("Height mode for item at \(indexPath) is out of bounds") return nil } @@ -124,8 +124,8 @@ final class ModelState { func itemModelPreferredHeight(at indexPath: IndexPath) -> CGFloat? { guard indexPath.section < sectionModels.count, - indexPath.item < sectionModels[indexPath.section].numberOfItems else - { + indexPath.item < sectionModels[indexPath.section].numberOfItems + else { assertionFailure("Height mode for item at \(indexPath) is out of bounds") return nil } @@ -134,63 +134,70 @@ final class ModelState { } func itemLocationFramePairs(forItemsIn rect: CGRect) -> ElementLocationFramePairs { - return elementLocationFramePairsForElements( + elementLocationFramePairsForElements( in: rect, withElementLocationsForFlattenedIndices: itemLocationsForFlattenedIndices, andFramesProvidedBy: { itemLocation -> CGRect in return frameForItem(at: itemLocation) - }) + } + ) } func headerLocationFramePairs(forHeadersIn rect: CGRect) -> ElementLocationFramePairs { - return elementLocationFramePairsForElements( + elementLocationFramePairsForElements( in: rect, withElementLocationsForFlattenedIndices: headerLocationsForFlattenedIndices, andFramesProvidedBy: { headerLocation -> CGRect in guard let headerFrame = frameForHeader( - inSectionAtIndex: headerLocation.sectionIndex) else - { + inSectionAtIndex: headerLocation.sectionIndex + ) + else { assertionFailure("Expected a frame for header in section at \(headerLocation.sectionIndex)") return .zero } return headerFrame - }) + } + ) } func footerLocationFramePairs(forFootersIn rect: CGRect) -> ElementLocationFramePairs { - return elementLocationFramePairsForElements( + elementLocationFramePairsForElements( in: rect, withElementLocationsForFlattenedIndices: footerLocationsForFlattenedIndices, andFramesProvidedBy: { footerLocation -> CGRect in guard let footerFrame = frameForFooter( - inSectionAtIndex: footerLocation.sectionIndex) else - { + inSectionAtIndex: footerLocation.sectionIndex + ) + else { assertionFailure("Expected a frame for footer in section at \(footerLocation.sectionIndex)") return .zero } return footerFrame - }) + } + ) } func backgroundLocationFramePairs(forBackgroundsIn rect: CGRect) -> ElementLocationFramePairs { - return elementLocationFramePairsForElements( + elementLocationFramePairsForElements( in: rect, withElementLocationsForFlattenedIndices: backgroundLocationsForFlattenedIndices, andFramesProvidedBy: { backgroundLocation -> CGRect in guard let backgroundFrame = frameForBackground( - inSectionAtIndex: backgroundLocation.sectionIndex) else - { + inSectionAtIndex: backgroundLocation.sectionIndex + ) + else { assertionFailure("Expected a frame for background in section at \(backgroundLocation.sectionIndex)") return .zero } return backgroundFrame - }) + } + ) } func sectionMaxY(forSectionAtIndex targetSectionIndex: Int) -> CGFloat { @@ -219,15 +226,18 @@ final class ModelState { sectionMinY = 0 } else { sectionMinY = sectionMaxY( - forSectionAtIndex: itemLocation.sectionIndex - 1) + forSectionAtIndex: itemLocation.sectionIndex - 1 + ) } var itemFrame: CGRect! mutateSectionModels( withUnsafeMutableBufferPointer: { directlyMutableSectionModels in itemFrame = directlyMutableSectionModels[itemLocation.sectionIndex].calculateFrameForItem( - atIndex: itemLocation.elementIndex) - }) + atIndex: itemLocation.elementIndex + ) + } + ) itemFrame.origin.y += sectionMinY return itemFrame @@ -250,8 +260,11 @@ final class ModelState { x: currentVisibleBounds.minX, y: currentVisibleBounds.minY - sectionMinY, width: currentVisibleBounds.width, - height: currentVisibleBounds.height)) - }) + height: currentVisibleBounds.height + ) + ) + } + ) headerFrame?.origin.y += sectionMinY return headerFrame @@ -274,8 +287,11 @@ final class ModelState { x: currentVisibleBounds.minX, y: currentVisibleBounds.minY - sectionMinY, width: currentVisibleBounds.width, - height: currentVisibleBounds.height)) - }) + height: currentVisibleBounds.height + ) + ) + } + ) footerFrame?.origin.y += sectionMinY return footerFrame @@ -293,7 +309,8 @@ final class ModelState { mutateSectionModels( withUnsafeMutableBufferPointer: { directlyMutableSectionModels in backgroundFrame = directlyMutableSectionModels[sectionIndex].calculateFrameForBackground() - }) + } + ) backgroundFrame?.origin.y += sectionMinY return backgroundFrame @@ -312,27 +329,28 @@ final class ModelState { func updateItemHeight( toPreferredHeight preferredHeight: CGFloat, - forItemAt indexPath: IndexPath) - { + forItemAt indexPath: IndexPath + ) { guard indexPath.section < sectionModels.count, - indexPath.item < sectionModels[indexPath.section].numberOfItems else - { + indexPath.item < sectionModels[indexPath.section].numberOfItems + else { assertionFailure("Updating the preferred height for an item model at \(indexPath) is out of bounds") return } sectionModels[indexPath.section].updateItemHeight( toPreferredHeight: preferredHeight, - atIndex: indexPath.item) + atIndex: indexPath.item + ) invalidateSectionMaxYsCacheForSectionIndices(startingAt: indexPath.section) } func updateHeaderHeight( toPreferredHeight preferredHeight: CGFloat, - forSectionAtIndex sectionIndex: Int) - { + forSectionAtIndex sectionIndex: Int + ) { guard sectionIndex < sectionModels.count else { assertionFailure("Updating the preferred height for a header model at section index \(sectionIndex) is out of bounds") return @@ -345,8 +363,8 @@ final class ModelState { func updateFooterHeight( toPreferredHeight preferredHeight: CGFloat, - forSectionAtIndex sectionIndex: Int) - { + forSectionAtIndex sectionIndex: Int + ) { guard sectionIndex < sectionModels.count else { assertionFailure("Updating the preferred height for a footer model at section index \(sectionIndex) is out of bounds") return @@ -359,8 +377,8 @@ final class ModelState { func updateMetrics( to sectionMetrics: MagazineLayoutSectionMetrics, - forSectionAtIndex sectionIndex: Int) - { + forSectionAtIndex sectionIndex: Int + ) { sectionModels[sectionIndex].updateMetrics(to: sectionMetrics) invalidateSectionMaxYsCacheForSectionIndices(startingAt: sectionIndex) } @@ -424,8 +442,8 @@ final class ModelState { func applyUpdates( _ updates: [CollectionViewUpdate], - modelStateBeforeBatchUpdates: ModelState) - { + modelStateBeforeBatchUpdates: ModelState + ) { let sectionModelsBeforeBatchUpdates = modelStateBeforeBatchUpdates.sectionModels invalidateEntireSectionMaxYsCache() @@ -440,37 +458,38 @@ final class ModelState { for update in updates { switch update { - case let .sectionReload(sectionIndex, newSection): + case .sectionReload(let sectionIndex, let newSection): sectionModelReloadIndexPairs.append((newSection, sectionIndex)) - case let .itemReload(itemIndexPath, newItem): + case .itemReload(let itemIndexPath, let newItem): itemModelReloadIndexPathPairs.append((newItem, itemIndexPath)) - case let .sectionDelete(sectionIndex): + case .sectionDelete(let sectionIndex): sectionIndicesToDelete.append(sectionIndex) self.sectionIndicesToDelete.insert(sectionIndex) - case let .itemDelete(itemIndexPath): + case .itemDelete(let itemIndexPath): itemIndexPathsToDelete.append(itemIndexPath) self.itemIndexPathsToDelete.insert(itemIndexPath) - case let .sectionMove(initialSectionIndex, finalSectionIndex): + case .sectionMove(let initialSectionIndex, let finalSectionIndex): sectionIndicesToDelete.append(initialSectionIndex) let sectionModelToMove = sectionModelsBeforeBatchUpdates[initialSectionIndex] sectionModelInsertIndexPairs.append((sectionModelToMove, finalSectionIndex)) - case let .itemMove(initialItemIndexPath, finalItemIndexPath): + case .itemMove(let initialItemIndexPath, let finalItemIndexPath): itemIndexPathsToDelete.append(initialItemIndexPath) let sectionContainingItemModelToMove = sectionModelsBeforeBatchUpdates[initialItemIndexPath.section] let itemModelToMove = sectionContainingItemModelToMove.itemModel( - atIndex: initialItemIndexPath.item) + atIndex: initialItemIndexPath.item + ) itemModelInsertIndexPathPairs.append((itemModelToMove, finalItemIndexPath)) - case let .sectionInsert(sectionIndex, newSection): + case .sectionInsert(let sectionIndex, let newSection): sectionModelInsertIndexPairs.append((newSection, sectionIndex)) sectionIndicesToInsert.insert(sectionIndex) - case let .itemInsert(itemIndexPath, newItem): + case .itemInsert(let itemIndexPath, let newItem): itemModelInsertIndexPathPairs.append((newItem, itemIndexPath)) itemIndexPathsToInsert.insert(itemIndexPath) } @@ -497,6 +516,19 @@ final class ModelState { itemIndexPathsToDelete.removeAll() } + func invalidateSectionMaxYsCacheForSectionIndices(startingAt sectionIndex: Int) { + guard sectionIndex >= 0, sectionIndex < sectionMaxYsCache.count else { + assertionFailure( + "Cannot invalidate `sectionMaxYsCache` starting at an invalid (negative or out-of-bounds) `sectionIndex` (\(sectionIndex))." + ) + return + } + + for sectionIndex in sectionIndex.. CGRect @@ -511,8 +543,8 @@ final class ModelState { private var itemLocationsForFlattenedIndices = [Int: ElementLocation]() private func mutateSectionModels( - withUnsafeMutableBufferPointer body: (inout UnsafeMutableBufferPointer) -> Void) - { + withUnsafeMutableBufferPointer body: (inout UnsafeMutableBufferPointer) -> Void + ) { // Accessing these arrays using unsafe, untyped (raw) pointers // avoids expensive copy-on-writes and Swift retain / release calls. sectionModels.withUnsafeMutableBufferPointer(body) @@ -532,28 +564,32 @@ final class ModelState { if sectionModels[sectionIndex].headerModel != nil { headerLocationsForFlattenedIndices[flattenedHeaderIndex] = ElementLocation( elementIndex: 0, - sectionIndex: sectionIndex) + sectionIndex: sectionIndex + ) flattenedHeaderIndex += 1 } if sectionModels[sectionIndex].footerModel != nil { footerLocationsForFlattenedIndices[flattenedFooterIndex] = ElementLocation( elementIndex: 0, - sectionIndex: sectionIndex) + sectionIndex: sectionIndex + ) flattenedFooterIndex += 1 } if sectionModels[sectionIndex].backgroundModel != nil { backgroundLocationsForFlattenedIndices[flattenedBackgroundIndex] = ElementLocation( elementIndex: 0, - sectionIndex: sectionIndex) + sectionIndex: sectionIndex + ) flattenedBackgroundIndex += 1 } for itemIndex in 0.. CGRect)) - -> ElementLocationFramePairs - { + andFramesProvidedBy frameProvider: (ElementLocation) -> CGRect + ) -> ElementLocationFramePairs { var elementLocationFramePairs = ElementLocationFramePairs() guard let indexOfFirstFoundElement = indexOfFirstFoundElement( in: rect, withElementLocationsForFlattenedIndices: elementLocationsForFlattenedIndices, - andFramesProvidedBy: frameProvider) else - { + andFramesProvidedBy: frameProvider + ) + else { return elementLocationFramePairs } @@ -584,7 +620,8 @@ final class ModelState { for elementLocationIndex in (0.. rect.minY else { @@ -601,19 +638,22 @@ final class ModelState { } elementLocationFramePairs.append( - ElementLocationFramePair(elementLocation: elementLocation, frame: frame)) + ElementLocationFramePair(elementLocation: elementLocation, frame: frame) + ) } // Look forward to find visible elements for elementLocationIndex in indexOfFirstFoundElement.. CGRect)) - -> Int? - { + andFramesProvidedBy frameProvider: (ElementLocation) -> CGRect + ) -> Int? { var lowerBound = 0 var upperBound = elementLocationsForFlattenedIndices.count - 1 @@ -632,7 +671,8 @@ final class ModelState { let index = (lowerBound + upperBound) / 2 let elementLocation = self.elementLocation( forFlattenedIndex: index, - in: elementLocationsForFlattenedIndices) + in: elementLocationsForFlattenedIndices + ) let elementFrame = frameProvider(elementLocation) if elementFrame.maxY <= rect.minY { lowerBound = index + 1 @@ -648,11 +688,12 @@ final class ModelState { private func elementLocation( forFlattenedIndex index: Int, - in elementLocationsForFlattenedIndices: [Int: ElementLocation]) - -> ElementLocation - { + in elementLocationsForFlattenedIndices: [Int: ElementLocation] + ) -> ElementLocation { guard let elementLocation = elementLocationsForFlattenedIndices[index] else { - preconditionFailure("`elementLocationsForFlattenedIndices` must have a complete mapping of indices in 0..<\(elementLocationsForFlattenedIndices.count) to element locations") + preconditionFailure( + "`elementLocationsForFlattenedIndices` must have a complete mapping of indices in 0..<\(elementLocationsForFlattenedIndices.count) to element locations" + ) } return elementLocation @@ -673,13 +714,13 @@ final class ModelState { } private func cachedMaxYForSection(atIndex sectionIndex: Int) -> CGFloat? { - guard sectionIndex >= 0 && sectionIndex < sectionMaxYsCache.count else { return nil } + guard sectionIndex >= 0, sectionIndex < sectionMaxYsCache.count else { return nil } return sectionMaxYsCache[sectionIndex] } private func cacheMaxY(_ sectionMaxY: CGFloat, forSectionAtIndex sectionIndex: Int) { - guard sectionIndex >= 0 && sectionIndex < sectionMaxYsCache.count else { return } + guard sectionIndex >= 0, sectionIndex < sectionMaxYsCache.count else { return } sectionMaxYsCache[sectionIndex] = sectionMaxY } @@ -690,20 +731,9 @@ final class ModelState { invalidateSectionMaxYsCacheForSectionIndices(startingAt: 0) } - func invalidateSectionMaxYsCacheForSectionIndices(startingAt sectionIndex: Int) { - guard sectionIndex >= 0, sectionIndex < sectionMaxYsCache.count else { - assertionFailure("Cannot invalidate `sectionMaxYsCache` starting at an invalid (negative or out-of-bounds) `sectionIndex` (\(sectionIndex)).") - return - } - - for sectionIndex in sectionIndex.. $1 }) { sectionModels[indexPathOfItemModelToDelete.section].deleteItemModel( - atIndex: indexPathOfItemModelToDelete.item) + atIndex: indexPathOfItemModelToDelete.item + ) } } private func insertSectionModels( - sectionModelInsertIndexPairs: [(sectionModel: SectionModel, insertIndex: Int)]) - { + sectionModelInsertIndexPairs: [(sectionModel: SectionModel, insertIndex: Int)] + ) { // Always insert in ascending order for (sectionModel, insertIndex) in (sectionModelInsertIndexPairs.sorted { $0.insertIndex < $1.insertIndex }) { sectionModels.insert(sectionModel, at: insertIndex) @@ -747,8 +781,8 @@ final class ModelState { } private func insertItemModels( - itemModelInsertIndexPathPairs: [(itemModel: ItemModel, insertIndexPath: IndexPath)]) - { + itemModelInsertIndexPathPairs: [(itemModel: ItemModel, insertIndexPath: IndexPath)] + ) { // Always insert in ascending order for (itemModel, insertIndexPath) in (itemModelInsertIndexPathPairs.sorted { $0.insertIndexPath < $1.insertIndexPath }) { let sectionIndex = insertIndexPath.section diff --git a/MagazineLayout/LayoutCore/SectionModel.swift b/MagazineLayout/LayoutCore/SectionModel.swift index 404ae81..b90c28e 100755 --- a/MagazineLayout/LayoutCore/SectionModel.swift +++ b/MagazineLayout/LayoutCore/SectionModel.swift @@ -26,8 +26,8 @@ struct SectionModel { headerModel: HeaderModel?, footerModel: FooterModel?, backgroundModel: BackgroundModel?, - metrics: MagazineLayoutSectionMetrics) - { + metrics: MagazineLayoutSectionMetrics + ) { id = idGenerator.next() self.itemModels = itemModels self.headerModel = headerModel @@ -52,28 +52,28 @@ struct SectionModel { var visibleBounds: CGRect? var numberOfItems: Int { - return itemModels.count + itemModels.count } func idForItemModel(atIndex index: Int) -> UInt64 { - return itemModels[index].id + itemModels[index].id } func indexForItemModel(withID id: UInt64) -> Int? { - return itemModels.firstIndex { $0.id == id } + itemModels.firstIndex { $0.id == id } } func itemModel(atIndex index: Int) -> ItemModel { - return itemModels[index] + itemModels[index] } func preferredHeightForItemModel(atIndex index: Int) -> CGFloat? { - return itemModels[index].preferredHeight + itemModels[index].preferredHeight } mutating func calculateHeight() -> CGFloat { calculateElementFramesIfNecessary() - + return calculatedHeight } @@ -91,9 +91,8 @@ struct SectionModel { } mutating func calculateFrameForHeader( - inSectionVisibleBounds sectionVisibleBounds: CGRect) - -> CGRect? - { + inSectionVisibleBounds sectionVisibleBounds: CGRect + ) -> CGRect? { guard headerModel != nil else { return nil } calculateElementFramesIfNecessary() @@ -110,24 +109,26 @@ struct SectionModel { calculateHeight() - metrics.sectionInsets.bottom - (footerModel?.size.height ?? 0) - - headerModel.size.height), - headerModel.originInSection.y) + headerModel.size.height + ), + headerModel.originInSection.y + ) } else { originY = headerModel.originInSection.y } return CGRect( origin: CGPoint(x: headerModel.originInSection.x, y: originY), - size: headerModel.size) + size: headerModel.size + ) } else { return nil } } mutating func calculateFrameForFooter( - inSectionVisibleBounds sectionVisibleBounds: CGRect) - -> CGRect? - { + inSectionVisibleBounds sectionVisibleBounds: CGRect + ) -> CGRect? { guard footerModel != nil else { return nil } calculateElementFramesIfNecessary() @@ -148,15 +149,18 @@ struct SectionModel { originY = min( max( sectionVisibleBounds.maxY - footerModel.size.height, - metrics.sectionInsets.top + (headerModel?.size.height ?? 0)), - origin.y) + metrics.sectionInsets.top + (headerModel?.size.height ?? 0) + ), + origin.y + ) } else { originY = origin.y } return CGRect( origin: CGPoint(x: footerModel.originInSection.x, y: originY), - size: footerModel.size) + size: footerModel.size + ) } else { return nil } @@ -167,7 +171,8 @@ struct SectionModel { backgroundModel?.originInSection = CGPoint( x: metrics.sectionInsets.left, - y: metrics.sectionInsets.top) + y: metrics.sectionInsets.top + ) backgroundModel?.size.width = metrics.width backgroundModel?.size.height = calculatedHeight - metrics.sectionInsets.top - @@ -176,7 +181,8 @@ struct SectionModel { if let backgroundModel = backgroundModel { return CGRect( origin: CGPoint(x: backgroundModel.originInSection.x, y: backgroundModel.originInSection.y), - size: backgroundModel.size) + size: backgroundModel.size + ) } else { return nil } @@ -191,7 +197,7 @@ struct SectionModel { mutating func insert(_ itemModel: ItemModel, atIndex indexOfInsertion: Int) { updateIndexOfFirstInvalidatedRow(forChangeToItemAtIndex: indexOfInsertion) - + itemModels.insert(itemModel, at: indexOfInsertion) } @@ -209,7 +215,7 @@ struct SectionModel { itemModels.withUnsafeMutableBufferPointer { directlyMutableItemModels in directlyMutableItemModels[index].sizeMode = sizeMode - if case let .static(staticHeight) = sizeMode.heightMode { + if case .static(let staticHeight) = sizeMode.heightMode { directlyMutableItemModels[index].size.height = staticHeight } } @@ -221,7 +227,7 @@ struct SectionModel { let oldPreferredHeight = self.headerModel?.preferredHeight self.headerModel = headerModel - if case let .static(staticHeight) = headerModel.heightMode { + if case .static(let staticHeight) = headerModel.heightMode { self.headerModel?.size.height = staticHeight } else if case .dynamic = headerModel.heightMode { self.headerModel?.preferredHeight = oldPreferredHeight @@ -236,7 +242,7 @@ struct SectionModel { let oldPreferredHeight = self.footerModel?.preferredHeight self.footerModel = footerModel - if case let .static(staticHeight) = footerModel.heightMode { + if case .static(let staticHeight) = footerModel.heightMode { self.footerModel?.size.height = staticHeight } else if case .dynamic = footerModel.heightMode { self.footerModel?.preferredHeight = oldPreferredHeight @@ -298,9 +304,9 @@ struct SectionModel { let rowHeight = headerModel.size.height let newRowHeight = updateHeaderHeight(withMetricsFrom: headerModel) let heightDelta = newRowHeight - rowHeight - + calculatedHeight += heightDelta - + let firstAffectedRowIndex = indexOfHeaderRow + 1 if firstAffectedRowIndex < numberOfRows { rowOffsetTracker?.addOffset(heightDelta, forRowsStartingAt: firstAffectedRowIndex) @@ -318,9 +324,9 @@ struct SectionModel { let rowHeight = footerModel.size.height let newRowHeight = updateFooterHeight(withMetricsFrom: footerModel) let heightDelta = newRowHeight - rowHeight - + calculatedHeight += heightDelta - + let firstAffectedRowIndex = indexOfFooterRow + 1 if firstAffectedRowIndex < numberOfRows { rowOffsetTracker?.addOffset(heightDelta, forRowsStartingAt: firstAffectedRowIndex) @@ -330,7 +336,7 @@ struct SectionModel { return } } - + mutating func setBackground(_ backgroundModel: BackgroundModel) { self.backgroundModel = backgroundModel // No need to invalidate since the background doesn't affect the layout. @@ -340,7 +346,7 @@ struct SectionModel { guard backgroundModel != nil else { return false } - self.backgroundModel = nil + backgroundModel = nil // No need to invalidate since the background doesn't affect the layout. return true } @@ -352,6 +358,12 @@ struct SectionModel { private var metrics: MagazineLayoutSectionMetrics private var calculatedHeight: CGFloat + private var itemIndicesForRowIndices = [Int: [Int]]() + private var rowIndicesForItemIndices = [Int: Int]() + private var itemRowHeightsForRowIndices = [Int: CGFloat]() + + private var rowOffsetTracker: RowOffsetTracker? + private var indexOfFirstInvalidatedRow: Int? { didSet { guard indexOfFirstInvalidatedRow != nil else { return } @@ -359,18 +371,12 @@ struct SectionModel { } } - private var itemIndicesForRowIndices = [Int: [Int]]() - private var rowIndicesForItemIndices = [Int: Int]() - private var itemRowHeightsForRowIndices = [Int: CGFloat]() - - private var rowOffsetTracker: RowOffsetTracker? - private func maxYForItemsRow(atIndex rowIndex: Int) -> CGFloat? { guard let itemIndices = itemIndicesForRowIndices[rowIndex], let itemY = itemIndices.first.flatMap({ itemModels[$0].originInSection.y }), - let itemHeight = itemIndices.map({ itemModels[$0].size.height }).max() else - { + let itemHeight = itemIndices.map({ itemModels[$0].size.height }).max() + else { return nil } @@ -396,25 +402,25 @@ struct SectionModel { guard footerModel != nil else { return nil } return numberOfRows - 1 } - + private mutating func updateIndexOfFirstInvalidatedRow(forChangeToItemAtIndex changedIndex: Int) { guard let indexOfCurrentRow = rowIndicesForItemIndices[changedIndex], - indexOfCurrentRow > 0 else - { + indexOfCurrentRow > 0 + else { indexOfFirstInvalidatedRow = rowIndicesForItemIndices[0] ?? 0 return } - + updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfCurrentRow - 1) } - + private mutating func updateIndexOfFirstInvalidatedRowIfNecessary( - toProposedIndex proposedIndex: Int) - { + toProposedIndex proposedIndex: Int + ) { indexOfFirstInvalidatedRow = min(proposedIndex, indexOfFirstInvalidatedRow ?? proposedIndex) } - + private mutating func applyRowOffsetsIfNecessary() { guard let rowOffsetTracker = rowOffsetTracker else { return } @@ -461,7 +467,8 @@ struct SectionModel { headerModel?.originInSection = CGPoint( x: metrics.sectionInsets.left, - y: metrics.sectionInsets.top) + y: metrics.sectionInsets.top + ) headerModel?.size.width = metrics.width updateHeaderHeight(withMetricsFrom: existingHeaderModel) } @@ -506,7 +513,7 @@ struct SectionModel { itemIndicesForRowIndices[rowIndex] = itemIndicesForRowIndices[rowIndex] ?? [] itemIndicesForRowIndices[rowIndex]?.append(itemIndex) rowIndicesForItemIndices[itemIndex] = rowIndex - + let itemModel = itemModels[itemIndex] if itemIndex == 0 { @@ -541,8 +548,8 @@ struct SectionModel { if (indexInCurrentRow == Int(itemModel.sizeMode.widthMode.widthDivisor) - 1) || - (itemIndex == numberOfItems - 1) || - (itemIndex < numberOfItems - 1 && itemModels[itemIndex + 1].sizeMode.widthMode != itemModel.sizeMode.widthMode) + (itemIndex == numberOfItems - 1) || + (itemIndex < numberOfItems - 1 && itemModels[itemIndex + 1].sizeMode.widthMode != itemModel.sizeMode.widthMode) { // We've reached the end of the current row, or there are no more items to lay out, or we're // about to lay out an item with a different width mode. In all cases, we're done laying out @@ -618,7 +625,7 @@ struct SectionModel { heightOfTallestItem = max(heightOfTallestItem, itemModels[itemIndex].size.height) } - for stretchToTallestItemInRowItemIndex in stretchToTallestItemInRowItemIndices{ + for stretchToTallestItemInRowItemIndex in stretchToTallestItemInRowItemIndices { // Accessing this array using an unsafe, untyped (raw) pointer avoids expensive copy-on-writes // and Swift retain / release calls. itemModels.withUnsafeMutableBufferPointer { directlyMutableItemModels in @@ -629,19 +636,19 @@ struct SectionModel { itemRowHeightsForRowIndices[rowIndex] = heightOfTallestItem return heightOfTallestItem } - + @discardableResult private mutating func updateHeaderHeight(withMetricsFrom headerModel: HeaderModel) -> CGFloat { let height = headerModel.preferredHeight ?? headerModel.size.height self.headerModel?.size.height = height return height } - + @discardableResult private mutating func updateFooterHeight(withMetricsFrom footerModel: FooterModel) -> CGFloat { let height = footerModel.preferredHeight ?? footerModel.size.height self.footerModel?.size.height = height return height } - + } diff --git a/MagazineLayout/LayoutCore/Types/ElementLocation.swift b/MagazineLayout/LayoutCore/Types/ElementLocation.swift index f9f32c9..5fd6c4f 100644 --- a/MagazineLayout/LayoutCore/Types/ElementLocation.swift +++ b/MagazineLayout/LayoutCore/Types/ElementLocation.swift @@ -15,6 +15,8 @@ import Foundation +// MARK: - ElementLocation + /// Represents the location of an item in a section. /// /// Initializing a `ElementLocation` is measurably faster than initializing an `IndexPath`. @@ -48,7 +50,7 @@ struct ElementLocation: Hashable { let sectionIndex: Int var indexPath: IndexPath { - return IndexPath(item: elementIndex, section: sectionIndex) + IndexPath(item: elementIndex, section: sectionIndex) } } @@ -57,7 +59,7 @@ struct ElementLocation: Hashable { extension ElementLocation: Comparable { - static func < (lhs: ElementLocation, rhs: ElementLocation) -> Bool { + static func <(lhs: ElementLocation, rhs: ElementLocation) -> Bool { lhs.sectionIndex < rhs.sectionIndex || (lhs.sectionIndex == rhs.sectionIndex && lhs.elementIndex < rhs.elementIndex) } diff --git a/MagazineLayout/LayoutCore/Types/ElementLocationFramePairs.swift b/MagazineLayout/LayoutCore/Types/ElementLocationFramePairs.swift index 05e43ba..bc93c01 100644 --- a/MagazineLayout/LayoutCore/Types/ElementLocationFramePairs.swift +++ b/MagazineLayout/LayoutCore/Types/ElementLocationFramePairs.swift @@ -62,7 +62,7 @@ struct ElementLocationFramePairs { extension ElementLocationFramePairs: Sequence { func makeIterator() -> ElementLocationFramePairsIterator { - return ElementLocationFramePairsIterator(self) + ElementLocationFramePairsIterator(self) } } @@ -72,8 +72,6 @@ extension ElementLocationFramePairs: Sequence { /// Used for iterating through `ElementLocationFramePairs` instances struct ElementLocationFramePairsIterator: IteratorProtocol { - typealias Element = ElementLocationFramePair - // MARK: Lifecycle init(_ elementLocationFramePairs: ElementLocationFramePairs) { @@ -82,6 +80,8 @@ struct ElementLocationFramePairsIterator: IteratorProtocol { // MARK: Internal + typealias Element = ElementLocationFramePair + mutating func next() -> ElementLocationFramePair? { if lastReturnedElement == nil { lastReturnedElement = elementLocationFramePairs.first @@ -127,8 +127,8 @@ final class ElementLocationFramePair { extension ElementLocationFramePair: Equatable { - static func == (lhs: ElementLocationFramePair, rhs: ElementLocationFramePair) -> Bool { - return lhs.elementLocation == rhs.elementLocation && lhs.frame == rhs.frame + static func ==(lhs: ElementLocationFramePair, rhs: ElementLocationFramePair) -> Bool { + lhs.elementLocation == rhs.elementLocation && lhs.frame == rhs.frame } } diff --git a/MagazineLayout/LayoutCore/Types/MagazineLayoutItemWidthMode+WidthDivisor.swift b/MagazineLayout/LayoutCore/Types/MagazineLayoutItemWidthMode+WidthDivisor.swift index 4b233bd..27ac3c8 100644 --- a/MagazineLayout/LayoutCore/Types/MagazineLayoutItemWidthMode+WidthDivisor.swift +++ b/MagazineLayout/LayoutCore/Types/MagazineLayoutItemWidthMode+WidthDivisor.swift @@ -24,7 +24,7 @@ extension MagazineLayoutItemWidthMode { var widthDivisor: CGFloat { switch self { case .fullWidth: return 1 - case let .fractionalWidth(divisor): return CGFloat(divisor) + case .fractionalWidth(let divisor): return CGFloat(divisor) } } diff --git a/MagazineLayout/LayoutCore/Types/MagazineLayoutSectionMetrics.swift b/MagazineLayout/LayoutCore/Types/MagazineLayoutSectionMetrics.swift index 92498d3..134f5f8 100644 --- a/MagazineLayout/LayoutCore/Types/MagazineLayoutSectionMetrics.swift +++ b/MagazineLayout/LayoutCore/Types/MagazineLayoutSectionMetrics.swift @@ -24,8 +24,8 @@ struct MagazineLayoutSectionMetrics: Equatable { forSectionAtIndex sectionIndex: Int, in collectionView: UICollectionView, layout: UICollectionViewLayout, - delegate: UICollectionViewDelegateMagazineLayout) - { + delegate: UICollectionViewDelegateMagazineLayout + ) { collectionViewWidth = collectionView.bounds.width collectionViewContentInset = collectionView.adjustedContentInset @@ -33,22 +33,26 @@ struct MagazineLayoutSectionMetrics: Equatable { verticalSpacing = delegate.collectionView( collectionView, layout: layout, - verticalSpacingForElementsInSectionAtIndex: sectionIndex) + verticalSpacingForElementsInSectionAtIndex: sectionIndex + ) horizontalSpacing = delegate.collectionView( collectionView, layout: layout, - horizontalSpacingForItemsInSectionAtIndex: sectionIndex) + horizontalSpacingForItemsInSectionAtIndex: sectionIndex + ) sectionInsets = delegate.collectionView( collectionView, layout: layout, - insetsForSectionAtIndex: sectionIndex) + insetsForSectionAtIndex: sectionIndex + ) itemInsets = delegate.collectionView( collectionView, layout: layout, - insetsForItemsInSectionAtIndex: sectionIndex) + insetsForItemsInSectionAtIndex: sectionIndex + ) scale = collectionView.traitCollection.nonZeroDisplayScale } @@ -60,8 +64,8 @@ struct MagazineLayoutSectionMetrics: Equatable { horizontalSpacing: CGFloat, sectionInsets: UIEdgeInsets, itemInsets: UIEdgeInsets, - scale: CGFloat) - { + scale: CGFloat + ) { self.collectionViewWidth = collectionViewWidth self.collectionViewContentInset = collectionViewContentInset self.verticalSpacing = verticalSpacing @@ -73,37 +77,37 @@ struct MagazineLayoutSectionMetrics: Equatable { // MARK: Internal + let verticalSpacing: CGFloat + let horizontalSpacing: CGFloat + let sectionInsets: UIEdgeInsets + let itemInsets: UIEdgeInsets + let scale: CGFloat + var width: CGFloat { - return collectionViewWidth - + collectionViewWidth - collectionViewContentInset.left - collectionViewContentInset.right - sectionInsets.left - sectionInsets.right } - let verticalSpacing: CGFloat - let horizontalSpacing: CGFloat - let sectionInsets: UIEdgeInsets - let itemInsets: UIEdgeInsets - let scale: CGFloat - static func defaultSectionMetrics( forCollectionViewWidth width: CGFloat, verticalSpacing: CGFloat = MagazineLayout.Default.VerticalSpacing, horizontalSpacing: CGFloat = MagazineLayout.Default.HorizontalSpacing, sectionInsets: UIEdgeInsets = MagazineLayout.Default.SectionInsets, itemInsets: UIEdgeInsets = MagazineLayout.Default.ItemInsets, - scale: CGFloat) - -> MagazineLayoutSectionMetrics - { - return MagazineLayoutSectionMetrics( + scale: CGFloat + ) -> MagazineLayoutSectionMetrics { + MagazineLayoutSectionMetrics( collectionViewWidth: width, collectionViewContentInset: .zero, verticalSpacing: verticalSpacing, horizontalSpacing: horizontalSpacing, sectionInsets: sectionInsets, itemInsets: itemInsets, - scale: scale) + scale: scale + ) } // MARK: Private diff --git a/MagazineLayout/LayoutCore/Types/UITraitCollection+DisplayScale.swift b/MagazineLayout/LayoutCore/Types/UITraitCollection+DisplayScale.swift index df1ab3e..8f0ebad 100644 --- a/MagazineLayout/LayoutCore/Types/UITraitCollection+DisplayScale.swift +++ b/MagazineLayout/LayoutCore/Types/UITraitCollection+DisplayScale.swift @@ -17,9 +17,9 @@ import UIKit extension UITraitCollection { - // The documentation mentions that 0 is a possible value, so we guard against this. - // It's unclear whether values between 0 and 1 are possible, otherwise `max(scale, 1)` would - // suffice. + /// The documentation mentions that 0 is a possible value, so we guard against this. + /// It's unclear whether values between 0 and 1 are possible, otherwise `max(scale, 1)` would + /// suffice. var nonZeroDisplayScale: CGFloat { displayScale > 0 ? displayScale : 1 } diff --git a/MagazineLayout/Public/MagazineLayout.swift b/MagazineLayout/Public/MagazineLayout.swift index 9713781..98be59a 100755 --- a/MagazineLayout/Public/MagazineLayout.swift +++ b/MagazineLayout/Public/MagazineLayout.swift @@ -15,6 +15,8 @@ import UIKit +// MARK: - MagazineLayout + /// A collection view layout that can display items in a grid and list arrangement. /// /// Consumers should implement `UICollectionViewDelegateMagazineLayout`, which is used for all @@ -41,21 +43,21 @@ public final class MagazineLayout: UICollectionViewLayout { } // MARK: Public - - /// The vertical layout direction of items in the collection view. This property changes the behavior of - /// scroll-position-preservation when performing batch updates or when the collection view's bounds changes. - public var verticalLayoutDirection = MagazineLayoutVerticalLayoutDirection.topToBottom override public class var layoutAttributesClass: AnyClass { - return MagazineLayoutCollectionViewLayoutAttributes.self + MagazineLayoutCollectionViewLayoutAttributes.self } override public class var invalidationContextClass: AnyClass { - return MagazineLayoutInvalidationContext.self + MagazineLayoutInvalidationContext.self } + /// The vertical layout direction of items in the collection view. This property changes the behavior of + /// scroll-position-preservation when performing batch updates or when the collection view's bounds changes. + public var verticalLayoutDirection = MagazineLayoutVerticalLayoutDirection.topToBottom + override public var flipsHorizontallyInOppositeLayoutDirection: Bool { - return _flipsHorizontallyInOppositeLayoutDirection + _flipsHorizontallyInOppositeLayoutDirection } override public var collectionViewContentSize: CGSize { @@ -117,7 +119,8 @@ public final class MagazineLayout: UICollectionViewLayout { bounds: currentCollectionView.bounds, contentInset: contentInset, scale: scale, - verticalLayoutDirection: verticalLayoutDirection) + verticalLayoutDirection: verticalLayoutDirection + ) var sections = [SectionModel]() for sectionIndex in 0..]() @@ -193,27 +197,30 @@ public final class MagazineLayout: UICollectionViewLayout { if updateAction == .move { guard let initialIndexPath = indexPathBeforeUpdate, - let finalIndexPath = indexPathAfterUpdate else - { + let finalIndexPath = indexPathAfterUpdate + else { assertionFailure("`indexPathBeforeUpdate` and `indexPathAfterUpdate` cannot be `nil` for a `.move` update action") return } - if initialIndexPath.item == NSNotFound && finalIndexPath.item == NSNotFound { + if initialIndexPath.item == NSNotFound, finalIndexPath.item == NSNotFound { updates.append(.sectionMove( initialSectionIndex: initialIndexPath.section, - finalSectionIndex: finalIndexPath.section)) + finalSectionIndex: finalIndexPath.section + )) } else { updates.append(.itemMove( initialItemIndexPath: initialIndexPath, - finalItemIndexPath: finalIndexPath)) + finalItemIndexPath: finalIndexPath + )) } } } modelState.applyUpdates( updates, - modelStateBeforeBatchUpdates: layoutStateBeforeCollectionViewUpdates.modelState) + modelStateBeforeBatchUpdates: layoutStateBeforeCollectionViewUpdates.modelState + ) hasDataSourceCountInvalidationBeforeReceivingUpdateItems = false super.prepare(forCollectionViewUpdates: updateItems) @@ -225,11 +232,12 @@ public final class MagazineLayout: UICollectionViewLayout { itemLayoutAttributesForPendingAnimations.removeAll() supplementaryViewLayoutAttributesForPendingAnimations.removeAll() - if let layoutStateBeforeCollectionViewUpdates{ + if let layoutStateBeforeCollectionViewUpdates { let targetContentOffsetAnchor = layoutStateBeforeCollectionViewUpdates.targetContentOffsetAnchor let targetYOffset = layoutState.yOffset( for: targetContentOffsetAnchor, - isPerformingBatchUpdates: true) + isPerformingBatchUpdates: true + ) let context = MagazineLayoutInvalidationContext() context.invalidateLayoutMetrics = false context.contentOffsetAdjustment.y = targetYOffset - layoutState.bounds.minY @@ -252,7 +260,8 @@ public final class MagazineLayout: UICollectionViewLayout { bounds: oldBounds, contentInset: contentInset, scale: scale, - verticalLayoutDirection: verticalLayoutDirection) + verticalLayoutDirection: verticalLayoutDirection + ) } } @@ -263,9 +272,8 @@ public final class MagazineLayout: UICollectionViewLayout { } override public func layoutAttributesForElements( - in rect: CGRect) - -> [UICollectionViewLayoutAttributes]? - { + in rect: CGRect + ) -> [UICollectionViewLayoutAttributes]? { // This early return prevents an issue that causes overlapping / misplaced elements after an // off-screen batch update occurs. The root cause of this issue is that `UICollectionView` // expects `layoutAttributesForElementsInRect:` to return post-batch-update layout attributes @@ -313,7 +321,8 @@ public final class MagazineLayout: UICollectionViewLayout { } let backgroundLocationFramePairs = modelState.backgroundLocationFramePairs( - forBackgroundsIn: rect) + forBackgroundsIn: rect + ) for backgroundLocationFramePair in backgroundLocationFramePairs { let backgroundLocation = backgroundLocationFramePair.elementLocation let backgroundFrame = backgroundLocationFramePair.frame @@ -321,7 +330,8 @@ public final class MagazineLayout: UICollectionViewLayout { if let layoutAttributes = backgroundLayoutAttributes( for: backgroundLocation, - frame: backgroundFrame) + frame: backgroundFrame + ) { layoutAttributesInRect.append(layoutAttributes) } @@ -341,9 +351,8 @@ public final class MagazineLayout: UICollectionViewLayout { } override public func layoutAttributesForItem( - at indexPath: IndexPath) - -> UICollectionViewLayoutAttributes? - { + at indexPath: IndexPath + ) -> UICollectionViewLayoutAttributes? { // See comment in `layoutAttributesForElementsInRect:` for more details. guard !hasDataSourceCountInvalidationBeforeReceivingUpdateItems else { return nil } @@ -352,12 +361,13 @@ public final class MagazineLayout: UICollectionViewLayout { guard itemLocation.sectionIndex < modelState.numberOfSections, itemLocation.elementIndex < modelState.numberOfItems(inSectionAtIndex: itemLocation.sectionIndex) - else - { + else { // On iOS 9, `layoutAttributesForItem(at:)` can be invoked for an index path of a new item // before the layout is notified of this new item (through either `prepare` or // `prepare(forCollectionViewUpdates:)`). This seems to be fixed in iOS 10 and higher. - assertionFailure("`{\(itemLocation.sectionIndex), \(itemLocation.elementIndex)}` is out of bounds of the section models / item models array.") + assertionFailure( + "`{\(itemLocation.sectionIndex), \(itemLocation.elementIndex)}` is out of bounds of the section models / item models array." + ) // Returning `nil` rather than default/frameless layout attributes causes internal exceptions // within `UICollectionView`, which is why we don't return `nil` here. @@ -370,9 +380,8 @@ public final class MagazineLayout: UICollectionViewLayout { override public func layoutAttributesForSupplementaryView( ofKind elementKind: String, - at indexPath: IndexPath) - -> UICollectionViewLayoutAttributes? - { + at indexPath: IndexPath + ) -> UICollectionViewLayoutAttributes? { // See comment in `layoutAttributesForElementsInRect:` for more details. guard !hasDataSourceCountInvalidationBeforeReceivingUpdateItems else { return nil } @@ -380,19 +389,22 @@ public final class MagazineLayout: UICollectionViewLayout { if elementKind == MagazineLayout.SupplementaryViewKind.sectionHeader, let headerFrame = modelState.frameForHeader( - inSectionAtIndex: elementLocation.sectionIndex) + inSectionAtIndex: elementLocation.sectionIndex + ) { return headerLayoutAttributes(for: elementLocation, frame: headerFrame) } else if elementKind == MagazineLayout.SupplementaryViewKind.sectionFooter, let footerFrame = modelState.frameForFooter( - inSectionAtIndex: elementLocation.sectionIndex) + inSectionAtIndex: elementLocation.sectionIndex + ) { return footerLayoutAttributes(for: elementLocation, frame: footerFrame) } else if elementKind == MagazineLayout.SupplementaryViewKind.sectionBackground, let backgroundFrame = modelState.frameForBackground( - inSectionAtIndex: elementLocation.sectionIndex) + inSectionAtIndex: elementLocation.sectionIndex + ) { return backgroundLayoutAttributes(for: elementLocation, frame: backgroundFrame) } else { @@ -401,9 +413,8 @@ public final class MagazineLayout: UICollectionViewLayout { } override public func initialLayoutAttributesForAppearingItem( - at itemIndexPath: IndexPath) - -> UICollectionViewLayoutAttributes? - { + at itemIndexPath: IndexPath + ) -> UICollectionViewLayoutAttributes? { let attributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath) attributes?.frame = modelState.frameForItem(at: ElementLocation(indexPath: itemIndexPath)) @@ -416,20 +427,23 @@ public final class MagazineLayout: UICollectionViewLayout { currentCollectionView, layout: self, initialLayoutAttributesForInsertedItemAt: itemIndexPath, - byModifying: $0) + byModifying: $0 + ) } attributes?.transform = CGAffineTransform( translationX: 0, - y: targetContentOffsetCompensatingYOffsetForAppearingItem ?? 0, + y: targetContentOffsetCompensatingYOffsetForAppearingItem ?? 0 ) itemLayoutAttributesForPendingAnimations[itemIndexPath] = attributes } else if let movedItemID = modelState.idForItemModel(at: itemIndexPath), let initialIndexPath = layoutStateBeforeCollectionViewUpdates?.modelState.indexPathForItemModel( - withID: movedItemID), - let frame = layoutStateBeforeCollectionViewUpdates?.modelState.frameForItem(at: ElementLocation(indexPath: initialIndexPath)) + withID: movedItemID + ), + let frame = layoutStateBeforeCollectionViewUpdates?.modelState + .frameForItem(at: ElementLocation(indexPath: initialIndexPath)) { attributes?.frame = frame } @@ -438,9 +452,8 @@ public final class MagazineLayout: UICollectionViewLayout { } override public func finalLayoutAttributesForDisappearingItem( - at itemIndexPath: IndexPath) - -> UICollectionViewLayoutAttributes? - { + at itemIndexPath: IndexPath + ) -> UICollectionViewLayoutAttributes? { if modelState.itemIndexPathsToDelete.contains(itemIndexPath) || modelState.sectionIndicesToDelete.contains(itemIndexPath.section) @@ -451,14 +464,17 @@ public final class MagazineLayout: UICollectionViewLayout { currentCollectionView, layout: self, finalLayoutAttributesForRemovedItemAt: itemIndexPath, - byModifying: $0) + byModifying: $0 + ) } return attributes } else if let movedItemID = layoutStateBeforeCollectionViewUpdates?.modelState.idForItemModel( - at: itemIndexPath), + at: itemIndexPath + ), let finalIndexPath = modelState.indexPathForItemModel( - withID: movedItemID) + withID: movedItemID + ) { let attributes = layoutAttributesForItem(at: finalIndexPath)?.copy() as? UICollectionViewLayoutAttributes itemLayoutAttributesForPendingAnimations[finalIndexPath] = attributes @@ -470,95 +486,108 @@ public final class MagazineLayout: UICollectionViewLayout { override public func initialLayoutAttributesForAppearingSupplementaryElement( ofKind elementKind: String, - at elementIndexPath: IndexPath) - -> UICollectionViewLayoutAttributes? - { + at elementIndexPath: IndexPath + ) -> UICollectionViewLayoutAttributes? { // If a supplementary view's visibility changes to `.hidden` due to a data source change, this // function will get invoked with an `elementIndexPath` that crashes when its `section` is // accessed. guard !elementIndexPath.isEmpty else { return super.initialLayoutAttributesForAppearingSupplementaryElement( ofKind: elementKind, - at: elementIndexPath) + at: elementIndexPath + ) } if modelState.sectionIndicesToInsert.contains(elementIndexPath.section) { let attributes = layoutAttributesForSupplementaryView( ofKind: elementKind, - at: elementIndexPath)?.copy() as? UICollectionViewLayoutAttributes + at: elementIndexPath + )?.copy() as? UICollectionViewLayoutAttributes attributes.map { modifySupplementaryViewLayoutAttributesForInsertAnimation( $0, ofKind: elementKind, - at: elementIndexPath) + at: elementIndexPath + ) } supplementaryViewLayoutAttributesForPendingAnimations[elementIndexPath] = attributes return attributes } else if let movedSectionID = modelState.idForSectionModel( - atIndex: elementIndexPath.section), + atIndex: elementIndexPath.section + ), let initialSectionIndex = layoutStateBeforeCollectionViewUpdates?.modelState.indexForSectionModel( - withID: movedSectionID) + withID: movedSectionID + ) { let initialIndexPath = IndexPath(item: 0, section: initialSectionIndex) return previousLayoutAttributesForSupplementaryView( ofKind: elementKind, - at: initialIndexPath) + at: initialIndexPath + ) } else { return super.initialLayoutAttributesForAppearingSupplementaryElement( ofKind: elementKind, - at: elementIndexPath) + at: elementIndexPath + ) } } override public func finalLayoutAttributesForDisappearingSupplementaryElement( ofKind elementKind: String, - at elementIndexPath: IndexPath) - -> UICollectionViewLayoutAttributes? - { + at elementIndexPath: IndexPath + ) -> UICollectionViewLayoutAttributes? { // If a supplementary view's visibility changes to `.hidden` due to a data source change, this // function will get invoked with an `elementIndexPath` that crashes when its `section` is // accessed. guard !elementIndexPath.isEmpty else { return super.finalLayoutAttributesForDisappearingSupplementaryElement( ofKind: elementKind, - at: elementIndexPath) + at: elementIndexPath + ) } if modelState.sectionIndicesToDelete.contains(elementIndexPath.section) { let attributes = previousLayoutAttributesForSupplementaryView( ofKind: elementKind, - at: elementIndexPath) + at: elementIndexPath + ) attributes.map { modifySupplementaryViewLayoutAttributesForDeleteAnimation( $0, ofKind: elementKind, - at: elementIndexPath) + at: elementIndexPath + ) } return attributes } else if let movedSectionID = layoutStateBeforeCollectionViewUpdates?.modelState.idForSectionModel( - atIndex: elementIndexPath.section), + atIndex: elementIndexPath.section + ), let finalSectionIndex = modelState.indexForSectionModel( - withID: movedSectionID) + withID: movedSectionID + ) { let finalIndexPath = IndexPath(item: 0, section: finalSectionIndex) let attributes = layoutAttributesForSupplementaryView( ofKind: elementKind, - at: finalIndexPath)?.copy() as? UICollectionViewLayoutAttributes + at: finalIndexPath + )?.copy() as? UICollectionViewLayoutAttributes supplementaryViewLayoutAttributesForPendingAnimations[finalIndexPath] = attributes return attributes - } else { + } else { return super.finalLayoutAttributesForDisappearingSupplementaryElement( ofKind: elementKind, - at: elementIndexPath) + at: elementIndexPath + ) } } override public func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { let isSameWidth = currentCollectionView.bounds.size.width.isEqual( to: newBounds.size.width, - screenScale: scale) + screenScale: scale + ) let shouldInvalidateDueToSize: Bool switch verticalLayoutDirection { case .topToBottom: @@ -569,7 +598,8 @@ public final class MagazineLayout: UICollectionViewLayout { // size change due to the requirement of needing to preserve scroll position from the bottom let isSameHeight = currentCollectionView.bounds.size.height.isEqual( to: newBounds.size.height, - screenScale: scale) + screenScale: scale + ) shouldInvalidateDueToSize = !isSameWidth || !isSameHeight } @@ -577,11 +607,11 @@ public final class MagazineLayout: UICollectionViewLayout { } override public func invalidationContext( - forBoundsChange newBounds: CGRect) - -> UICollectionViewLayoutInvalidationContext - { + forBoundsChange newBounds: CGRect + ) -> UICollectionViewLayoutInvalidationContext { let invalidationContext = super.invalidationContext( - forBoundsChange: newBounds) as! MagazineLayoutInvalidationContext + forBoundsChange: newBounds + ) as! MagazineLayoutInvalidationContext invalidationContext.invalidateLayoutMetrics = false @@ -592,14 +622,16 @@ public final class MagazineLayout: UICollectionViewLayout { if newBounds.height < currentCollectionView.bounds.height { invalidationContext.contentOffsetAdjustment = CGPoint( x: 0.0, - y: currentCollectionView.bounds.midY - newBounds.midY) + y: currentCollectionView.bounds.midY - newBounds.midY + ) } else if newBounds.height > currentCollectionView.bounds.height { let distanceFromBottom = currentCollectionView.contentSize.height - currentCollectionView.bounds.maxY let midYDelta = newBounds.midY - currentCollectionView.bounds.midY let heightDelta = newBounds.height - currentCollectionView.bounds.height invalidationContext.contentOffsetAdjustment = CGPoint( x: 0.0, - y: midYDelta - min(distanceFromBottom, heightDelta)) + y: midYDelta - min(distanceFromBottom, heightDelta) + ) } } @@ -608,18 +640,19 @@ public final class MagazineLayout: UICollectionViewLayout { override public func shouldInvalidateLayout( forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, - withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) - -> Bool - { + withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes + ) -> Bool { guard !preferredAttributes.indexPath.isEmpty else { return super.shouldInvalidateLayout( forPreferredLayoutAttributes: preferredAttributes, - withOriginalAttributes: originalAttributes) + withOriginalAttributes: originalAttributes + ) } let isSameHeight = preferredAttributes.size.height.isEqual( to: originalAttributes.size.height, - screenScale: scale) + screenScale: scale + ) let hasNewPreferredHeight = !isSameHeight switch (preferredAttributes.representedElementCategory, preferredAttributes.representedElementKind) { @@ -628,27 +661,34 @@ public final class MagazineLayout: UICollectionViewLayout { switch itemHeightMode { case .some(.static): return false + case .some(.dynamic): return hasNewPreferredHeight + case .some(.dynamicAndStretchToTallestItemInRow): let currentPreferredHeight = modelState.itemModelPreferredHeight( - at: preferredAttributes.indexPath) + at: preferredAttributes.indexPath + ) let isSameHeight = preferredAttributes.size.height.isEqual( to: currentPreferredHeight ?? -.greatestFiniteMagnitude, - screenScale: scale) + screenScale: scale + ) return hasNewPreferredHeight && !isSameHeight + case nil: return false } case (.supplementaryView, MagazineLayout.SupplementaryViewKind.sectionHeader): let headerHeightMode = modelState.headerModelHeightMode( - atSectionIndex: preferredAttributes.indexPath.section) + atSectionIndex: preferredAttributes.indexPath.section + ) return headerHeightMode == .dynamic case (.supplementaryView, MagazineLayout.SupplementaryViewKind.sectionFooter): let footerHeightMode = modelState.footerModelHeightMode( - atSectionIndex: preferredAttributes.indexPath.section) + atSectionIndex: preferredAttributes.indexPath.section + ) return footerHeightMode == .dynamic case (.supplementaryView, MagazineLayout.SupplementaryViewKind.sectionBackground): @@ -662,12 +702,12 @@ public final class MagazineLayout: UICollectionViewLayout { override public func invalidationContext( forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, - withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) - -> UICollectionViewLayoutInvalidationContext - { + withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes + ) -> UICollectionViewLayoutInvalidationContext { let context = super.invalidationContext( forPreferredLayoutAttributes: preferredAttributes, - withOriginalAttributes: originalAttributes) as! MagazineLayoutInvalidationContext + withOriginalAttributes: originalAttributes + ) as! MagazineLayoutInvalidationContext context.invalidateLayoutMetrics = false switch preferredAttributes.representedElementCategory { @@ -676,15 +716,17 @@ public final class MagazineLayout: UICollectionViewLayout { layoutStateBeforeRecreateSectionModels ?? layoutStateBeforeCollectionViewUpdates ?? layoutStateBeforeAnimatedBoundsChange ?? - self.layoutState + layoutState ).targetContentOffsetAnchor let targetYOffsetBefore = layoutState.yOffset( for: targetContentOffsetAnchor, - isPerformingBatchUpdates: layoutStateBeforeCollectionViewUpdates != nil) + isPerformingBatchUpdates: layoutStateBeforeCollectionViewUpdates != nil + ) modelState.updateItemHeight( toPreferredHeight: preferredAttributes.size.height, - forItemAt: preferredAttributes.indexPath) + forItemAt: preferredAttributes.indexPath + ) switch targetContentOffsetAnchor { case .top: @@ -696,7 +738,8 @@ public final class MagazineLayout: UICollectionViewLayout { case .topItem, .bottomItem: let targetYOffsetAfter = layoutState.yOffset( for: targetContentOffsetAnchor, - isPerformingBatchUpdates: layoutStateBeforeCollectionViewUpdates != nil) + isPerformingBatchUpdates: layoutStateBeforeCollectionViewUpdates != nil + ) context.contentOffsetAdjustment.y = targetYOffsetAfter - targetYOffsetBefore } @@ -713,7 +756,8 @@ public final class MagazineLayout: UICollectionViewLayout { let previousHeight = attributes.frame.height attributes.frame = modelState.frameForItem(at: ElementLocation(indexPath: preferredAttributes.indexPath)) - var targetContentOffsetCompensatingYOffsetForAppearingItem = targetContentOffsetCompensatingYOffsetForAppearingItem ?? 0 + var targetContentOffsetCompensatingYOffsetForAppearingItem = targetContentOffsetCompensatingYOffsetForAppearingItem ?? + 0 targetContentOffsetCompensatingYOffsetForAppearingItem -= (attributes.frame.height - previousHeight) self.targetContentOffsetCompensatingYOffsetForAppearingItem = targetContentOffsetCompensatingYOffsetForAppearingItem attributes.transform = CGAffineTransform(translationX: 0, y: targetContentOffsetCompensatingYOffsetForAppearingItem) @@ -722,24 +766,29 @@ public final class MagazineLayout: UICollectionViewLayout { } case .supplementaryView: - let layoutAttributesForPendingAnimation = supplementaryViewLayoutAttributesForPendingAnimations[preferredAttributes.indexPath] + let layoutAttributesForPendingAnimation = + supplementaryViewLayoutAttributesForPendingAnimations[preferredAttributes.indexPath] switch preferredAttributes.representedElementKind { case MagazineLayout.SupplementaryViewKind.sectionHeader?: modelState.updateHeaderHeight( toPreferredHeight: preferredAttributes.size.height, - forSectionAtIndex: preferredAttributes.indexPath.section) + forSectionAtIndex: preferredAttributes.indexPath.section + ) layoutAttributesForPendingAnimation?.frame.size.height = modelState.frameForHeader( - inSectionAtIndex: preferredAttributes.indexPath.section)?.height ?? preferredAttributes.size.height + inSectionAtIndex: preferredAttributes.indexPath.section + )?.height ?? preferredAttributes.size.height case MagazineLayout.SupplementaryViewKind.sectionFooter?: modelState.updateFooterHeight( toPreferredHeight: preferredAttributes.size.height, - forSectionAtIndex: preferredAttributes.indexPath.section) + forSectionAtIndex: preferredAttributes.indexPath.section + ) layoutAttributesForPendingAnimation?.frame.size.height = modelState.frameForFooter( - inSectionAtIndex: preferredAttributes.indexPath.section)?.height ?? preferredAttributes.size.height + inSectionAtIndex: preferredAttributes.indexPath.section + )?.height ?? preferredAttributes.size.height default: break @@ -789,13 +838,14 @@ public final class MagazineLayout: UICollectionViewLayout { // because the collection view's width can change without a `contentSizeAdjustment` occurring. let isSameWidth = collectionView?.bounds.size.width.isEqual( to: cachedCollectionViewWidth ?? -.greatestFiniteMagnitude, - screenScale: scale) + screenScale: scale + ) ?? false if !isSameWidth { prepareActions.formUnion([.updateLayoutMetrics, .cachePreviousWidth]) } - if context.invalidateLayoutMetrics && shouldInvalidateLayoutMetrics { + if context.invalidateLayoutMetrics, shouldInvalidateLayoutMetrics { prepareActions.formUnion([.updateLayoutMetrics]) } @@ -815,9 +865,8 @@ public final class MagazineLayout: UICollectionViewLayout { } override public func targetContentOffset( - forProposedContentOffset proposedContentOffset: CGPoint) - -> CGPoint - { + forProposedContentOffset proposedContentOffset: CGPoint + ) -> CGPoint { let layoutStateBefore = layoutStateBeforeCollectionViewUpdates ?? layoutStateBeforeAnimatedBoundsChange guard let layoutStateBefore else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) @@ -825,7 +874,8 @@ public final class MagazineLayout: UICollectionViewLayout { let yOffset = layoutState.yOffset( for: layoutStateBefore.targetContentOffsetAnchor, - isPerformingBatchUpdates: layoutStateBeforeCollectionViewUpdates != nil) + isPerformingBatchUpdates: layoutStateBeforeCollectionViewUpdates != nil + ) targetContentOffsetCompensatingYOffsetForAppearingItem = proposedContentOffset.y - yOffset @@ -834,6 +884,15 @@ public final class MagazineLayout: UICollectionViewLayout { // MARK: Private + private struct PrepareActions: OptionSet { + static let recreateSectionModels = PrepareActions(rawValue: 1 << 0) + static let updateLayoutMetrics = PrepareActions(rawValue: 1 << 1) + static let cachePreviousWidth = PrepareActions(rawValue: 1 << 2) + + let rawValue: UInt + + } + private let _flipsHorizontallyInOppositeLayoutDirection: Bool private let idGenerator = IDGenerator() @@ -844,12 +903,13 @@ public final class MagazineLayout: UICollectionViewLayout { bounds: currentCollectionView.bounds, contentInset: contentInset, scale: scale, - verticalLayoutDirection: verticalLayoutDirection) + verticalLayoutDirection: verticalLayoutDirection + ) private var layoutStateBeforeRecreateSectionModels: LayoutState? private var layoutStateBeforeCollectionViewUpdates: LayoutState? private var layoutStateBeforeAnimatedBoundsChange: LayoutState? - private var hasPinnedHeaderOrFooter: Bool = false + private var hasPinnedHeaderOrFooter = false // Cached layout attributes; lazily populated using information from the model state. private var itemLayoutAttributes = [ElementLocation: MagazineLayoutCollectionViewLayoutAttributes]() @@ -863,18 +923,11 @@ public final class MagazineLayout: UICollectionViewLayout { private var itemLayoutAttributesForPendingAnimations = [IndexPath: UICollectionViewLayoutAttributes]() private var supplementaryViewLayoutAttributesForPendingAnimations = [IndexPath: UICollectionViewLayoutAttributes]() - // We need to apply the target content offset to the initial y-offset of an appearing item. - // Without this, the appearing item will be visually at the wrong spot, making it look like it - // slides into place rather than appearing at its final position. + /// We need to apply the target content offset to the initial y-offset of an appearing item. + /// Without this, the appearing item will be visually at the wrong spot, making it look like it + /// slides into place rather than appearing at its final position. private var targetContentOffsetCompensatingYOffsetForAppearingItem: CGFloat? - private struct PrepareActions: OptionSet { - let rawValue: UInt - - static let recreateSectionModels = PrepareActions(rawValue: 1 << 0) - static let updateLayoutMetrics = PrepareActions(rawValue: 1 << 1) - static let cachePreviousWidth = PrepareActions(rawValue: 1 << 2) - } private var prepareActions: PrepareActions = [] // Used to prevent a collection view bug / animation issue that occurs when off-screen batch @@ -893,8 +946,8 @@ public final class MagazineLayout: UICollectionViewLayout { return collectionView } - // Used to provide the model state with the current visible bounds for the sole purpose of - // supporting pinned headers and footers. + /// Used to provide the model state with the current visible bounds for the sole purpose of + /// supporting pinned headers and footers. private var currentVisibleBounds: CGRect { let refreshControlHeight: CGFloat #if os(iOS) @@ -914,11 +967,12 @@ public final class MagazineLayout: UICollectionViewLayout { x: currentCollectionView.bounds.minX + contentInset.left, y: currentCollectionView.bounds.minY + contentInset.top - refreshControlHeight, width: currentCollectionView.bounds.width - contentInset.left - contentInset.right, - height: currentCollectionView.bounds.height - contentInset.top - contentInset.bottom + refreshControlHeight) + height: currentCollectionView.bounds.height - contentInset.top - contentInset.bottom + refreshControlHeight + ) } private var delegateMagazineLayout: UICollectionViewDelegateMagazineLayout? { - return currentCollectionView.delegate as? UICollectionViewDelegateMagazineLayout + currentCollectionView.delegate as? UICollectionViewDelegateMagazineLayout } private var scale: CGFloat { @@ -945,34 +999,38 @@ public final class MagazineLayout: UICollectionViewLayout { guard let delegateMagazineLayout = delegateMagazineLayout else { return MagazineLayoutSectionMetrics.defaultSectionMetrics( forCollectionViewWidth: currentCollectionView.bounds.width, - scale: scale) + scale: scale + ) } return MagazineLayoutSectionMetrics( forSectionAtIndex: sectionIndex, in: currentCollectionView, layout: self, - delegate: delegateMagazineLayout) + delegate: delegateMagazineLayout + ) } private func sizeModeForItem(at indexPath: IndexPath) -> MagazineLayoutItemSizeMode { guard let delegateMagazineLayout = delegateMagazineLayout else { return MagazineLayoutItemSizeMode( widthMode: MagazineLayout.Default.ItemSizeMode.widthMode, - heightMode: .static(height: MagazineLayout.Default.ItemHeight)) + heightMode: .static(height: MagazineLayout.Default.ItemHeight) + ) } return delegateMagazineLayout.collectionView( currentCollectionView, layout: self, - sizeModeForItemAt: indexPath) + sizeModeForItemAt: indexPath + ) } private func initialItemHeight(from itemSizeMode: MagazineLayoutItemSizeMode) -> CGFloat { switch itemSizeMode.heightMode { - case let .static(staticHeight): + case .static(let staticHeight): return staticHeight - case let .dynamic(estimatedHeight): + case .dynamic(let estimatedHeight): return estimatedHeight case .dynamicAndStretchToTallestItemInRow: return MagazineLayout.Default.ItemHeight @@ -980,9 +1038,8 @@ public final class MagazineLayout: UICollectionViewLayout { } private func visibilityModeForHeader( - inSectionAtIndex sectionIndex: Int) - -> MagazineLayoutHeaderVisibilityMode - { + inSectionAtIndex sectionIndex: Int + ) -> MagazineLayoutHeaderVisibilityMode { guard let delegateMagazineLayout = delegateMagazineLayout else { return MagazineLayout.Default.HeaderVisibilityMode } @@ -990,13 +1047,13 @@ public final class MagazineLayout: UICollectionViewLayout { return delegateMagazineLayout.collectionView( currentCollectionView, layout: self, - visibilityModeForHeaderInSectionAtIndex: sectionIndex) + visibilityModeForHeaderInSectionAtIndex: sectionIndex + ) } private func visibilityModeForFooter( - inSectionAtIndex sectionIndex: Int) - -> MagazineLayoutFooterVisibilityMode - { + inSectionAtIndex sectionIndex: Int + ) -> MagazineLayoutFooterVisibilityMode { guard let delegateMagazineLayout = delegateMagazineLayout else { return MagazineLayout.Default.FooterVisibilityMode } @@ -1004,13 +1061,13 @@ public final class MagazineLayout: UICollectionViewLayout { return delegateMagazineLayout.collectionView( currentCollectionView, layout: self, - visibilityModeForFooterInSectionAtIndex: sectionIndex) + visibilityModeForFooterInSectionAtIndex: sectionIndex + ) } private func visibilityModeForBackground( - inSectionAtIndex sectionIndex: Int) - -> MagazineLayoutBackgroundVisibilityMode - { + inSectionAtIndex sectionIndex: Int + ) -> MagazineLayoutBackgroundVisibilityMode { guard let delegateMagazineLayout = delegateMagazineLayout else { return MagazineLayout.Default.BackgroundVisibilityMode } @@ -1018,15 +1075,15 @@ public final class MagazineLayout: UICollectionViewLayout { return delegateMagazineLayout.collectionView( currentCollectionView, layout: self, - visibilityModeForBackgroundInSectionAtIndex: sectionIndex) + visibilityModeForBackgroundInSectionAtIndex: sectionIndex + ) } private func headerHeight( - from headerHeightMode: MagazineLayoutHeaderHeightMode) - -> CGFloat - { + from headerHeightMode: MagazineLayoutHeaderHeightMode + ) -> CGFloat { switch headerHeightMode { - case let .static(staticHeight): + case .static(let staticHeight): return staticHeight case .dynamic: return MagazineLayout.Default.HeaderHeight @@ -1034,11 +1091,10 @@ public final class MagazineLayout: UICollectionViewLayout { } private func footerHeight( - from footerHeightMode: MagazineLayoutFooterHeightMode) - -> CGFloat - { + from footerHeightMode: MagazineLayoutFooterHeightMode + ) -> CGFloat { switch footerHeightMode { - case let .static(staticHeight): + case .static(let staticHeight): return staticHeight case .dynamic: return MagazineLayout.Default.FooterHeight @@ -1056,7 +1112,8 @@ public final class MagazineLayout: UICollectionViewLayout { headerModel: headerModelForHeader(inSectionAtIndex: sectionIndex), footerModel: footerModelForFooter(inSectionAtIndex: sectionIndex), backgroundModel: backgroundModelForBackground(inSectionAtIndex: sectionIndex), - metrics: metricsForSection(atIndex: sectionIndex)) + metrics: metricsForSection(atIndex: sectionIndex) + ) } private func itemModelForItem(at indexPath: IndexPath) -> ItemModel { @@ -1064,43 +1121,47 @@ public final class MagazineLayout: UICollectionViewLayout { return ItemModel( idGenerator: idGenerator, sizeMode: itemSizeMode, - height: initialItemHeight(from: itemSizeMode)) + height: initialItemHeight(from: itemSizeMode) + ) } private func headerModelForHeader( - inSectionAtIndex sectionIndex: Int) - -> HeaderModel? - { + inSectionAtIndex sectionIndex: Int + ) -> HeaderModel? { let headerVisibilityMode = visibilityModeForHeader(inSectionAtIndex: sectionIndex) switch headerVisibilityMode { - case let .visible(heightMode, pinToVisibleBounds): + case .visible(let heightMode, let pinToVisibleBounds): return HeaderModel( heightMode: heightMode, - height: headerHeight(from: heightMode), pinToVisibleBounds: pinToVisibleBounds) + height: headerHeight(from: heightMode), + pinToVisibleBounds: pinToVisibleBounds + ) + case .hidden: return nil } } private func footerModelForFooter( - inSectionAtIndex sectionIndex: Int) - -> FooterModel? - { + inSectionAtIndex sectionIndex: Int + ) -> FooterModel? { let footerVisibilityMode = visibilityModeForFooter(inSectionAtIndex: sectionIndex) switch footerVisibilityMode { - case let .visible(heightMode, pinToVisibleBounds): + case .visible(let heightMode, let pinToVisibleBounds): return FooterModel( heightMode: heightMode, - height: footerHeight(from: heightMode), pinToVisibleBounds: pinToVisibleBounds) + height: footerHeight(from: heightMode), + pinToVisibleBounds: pinToVisibleBounds + ) + case .hidden: return nil } } private func backgroundModelForBackground( - inSectionAtIndex sectionIndex: Int) - -> BackgroundModel? - { + inSectionAtIndex sectionIndex: Int + ) -> BackgroundModel? { let backgroundVisibilityMode = visibilityModeForBackground(inSectionAtIndex: sectionIndex) switch backgroundVisibilityMode { case .visible: @@ -1111,9 +1172,8 @@ public final class MagazineLayout: UICollectionViewLayout { } private func previousLayoutAttributesForItem( - at indexPath: IndexPath) - -> UICollectionViewLayoutAttributes? - { + at indexPath: IndexPath + ) -> UICollectionViewLayoutAttributes? { let layoutAttributes = MagazineLayoutCollectionViewLayoutAttributes(forCellWith: indexPath) guard let layoutStateBeforeCollectionViewUpdates else { @@ -1127,7 +1187,8 @@ public final class MagazineLayout: UICollectionViewLayout { guard indexPath.section < layoutStateBeforeCollectionViewUpdates.modelState.numberOfSections, indexPath.item < layoutStateBeforeCollectionViewUpdates.modelState.numberOfItems( - inSectionAtIndex: indexPath.section) + inSectionAtIndex: indexPath.section + ) else { // On iOS 9, `layoutAttributesForItem(at:)` can be invoked for an index path of a new item // before the layout is notified of this new item (through either `prepare` or @@ -1140,19 +1201,20 @@ public final class MagazineLayout: UICollectionViewLayout { } layoutAttributes.frame = layoutStateBeforeCollectionViewUpdates.modelState.frameForItem( - at: ElementLocation(indexPath: indexPath)) + at: ElementLocation(indexPath: indexPath) + ) return layoutAttributes } private func previousLayoutAttributesForSupplementaryView( ofKind elementKind: String, - at indexPath: IndexPath) - -> UICollectionViewLayoutAttributes? - { + at indexPath: IndexPath + ) -> UICollectionViewLayoutAttributes? { let layoutAttributes = MagazineLayoutCollectionViewLayoutAttributes( forSupplementaryViewOfKind: elementKind, - with: indexPath) + with: indexPath + ) guard let layoutStateBeforeCollectionViewUpdates else { // TODO(bryankeller): Look into whether this happens on iOS 10. It definitely does on iOS 9. @@ -1176,52 +1238,61 @@ public final class MagazineLayout: UICollectionViewLayout { if elementKind == MagazineLayout.SupplementaryViewKind.sectionHeader, let headerFrame = layoutStateBeforeCollectionViewUpdates.modelState.frameForHeader( - inSectionAtIndex: indexPath.section) + inSectionAtIndex: indexPath.section + ) { layoutAttributes.frame = headerFrame } else if elementKind == MagazineLayout.SupplementaryViewKind.sectionFooter, let footerFrame = layoutStateBeforeCollectionViewUpdates.modelState.frameForFooter( - inSectionAtIndex: indexPath.section) + inSectionAtIndex: indexPath.section + ) { layoutAttributes.frame = footerFrame } else if elementKind == MagazineLayout.SupplementaryViewKind.sectionBackground, let backgroundFrame = layoutStateBeforeCollectionViewUpdates.modelState.frameForBackground( - inSectionAtIndex: indexPath.section) + inSectionAtIndex: indexPath.section + ) { layoutAttributes.frame = backgroundFrame } else { assertionFailure("\(elementKind) is not a valid supplementary view element kind.") } - + return layoutAttributes } private func modifySupplementaryViewLayoutAttributesForInsertAnimation( _ attributes: UICollectionViewLayoutAttributes, ofKind elementKind: String, - at indexPath: IndexPath) - { + at indexPath: IndexPath + ) { switch elementKind { case MagazineLayout.SupplementaryViewKind.sectionHeader: delegateMagazineLayout?.collectionView( currentCollectionView, layout: self, initialLayoutAttributesForInsertedHeaderInSectionAtIndex: indexPath.section, - byModifying: attributes) + byModifying: attributes + ) + case MagazineLayout.SupplementaryViewKind.sectionFooter: delegateMagazineLayout?.collectionView( currentCollectionView, layout: self, initialLayoutAttributesForInsertedFooterInSectionAtIndex: indexPath.section, - byModifying: attributes) + byModifying: attributes + ) + case MagazineLayout.SupplementaryViewKind.sectionBackground: delegateMagazineLayout?.collectionView( currentCollectionView, layout: self, initialLayoutAttributesForInsertedBackgroundInSectionAtIndex: indexPath.section, - byModifying: attributes) + byModifying: attributes + ) + default: assertionFailure("\(elementKind) is not a valid supplementary view element kind.") } @@ -1230,27 +1301,33 @@ public final class MagazineLayout: UICollectionViewLayout { private func modifySupplementaryViewLayoutAttributesForDeleteAnimation( _ attributes: UICollectionViewLayoutAttributes, ofKind elementKind: String, - at indexPath: IndexPath) - { + at indexPath: IndexPath + ) { switch elementKind { case MagazineLayout.SupplementaryViewKind.sectionHeader: delegateMagazineLayout?.collectionView( currentCollectionView, layout: self, finalLayoutAttributesForRemovedHeaderInSectionAtIndex: indexPath.section, - byModifying: attributes) + byModifying: attributes + ) + case MagazineLayout.SupplementaryViewKind.sectionFooter: delegateMagazineLayout?.collectionView( currentCollectionView, layout: self, finalLayoutAttributesForRemovedFooterInSectionAtIndex: indexPath.section, - byModifying: attributes) + byModifying: attributes + ) + case MagazineLayout.SupplementaryViewKind.sectionBackground: delegateMagazineLayout?.collectionView( currentCollectionView, layout: self, finalLayoutAttributesForRemovedBackgroundInSectionAtIndex: indexPath.section, - byModifying: attributes) + byModifying: attributes + ) + default: assertionFailure("\(elementKind) is not a valid supplementary view element kind.") } @@ -1260,13 +1337,12 @@ public final class MagazineLayout: UICollectionViewLayout { // MARK: Layout Attributes Creation and Caching -private extension MagazineLayout { +extension MagazineLayout { - func headerLayoutAttributes( + private func headerLayoutAttributes( for headerLocation: ElementLocation, - frame: CGRect) - -> UICollectionViewLayoutAttributes? - { + frame: CGRect + ) -> UICollectionViewLayoutAttributes? { guard headerLocation.sectionIndex < currentCollectionView.numberOfSections else { return nil } let layoutAttributes: MagazineLayoutCollectionViewLayoutAttributes @@ -1278,15 +1354,17 @@ private extension MagazineLayout { } else { layoutAttributes = MagazineLayoutCollectionViewLayoutAttributes( forSupplementaryViewOfKind: MagazineLayout.SupplementaryViewKind.sectionHeader, - with: headerLocation.indexPath) + with: headerLocation.indexPath + ) } layoutAttributes.frame = frame let sectionIndex = headerLocation.sectionIndex if - case let .visible(heightMode, pinToVisibleBounds) = visibilityModeForHeader( - inSectionAtIndex: sectionIndex) + case .visible(let heightMode, let pinToVisibleBounds) = visibilityModeForHeader( + inSectionAtIndex: sectionIndex + ) { layoutAttributes.shouldVerticallySelfSize = heightMode == .dynamic hasPinnedHeaderOrFooter = hasPinnedHeaderOrFooter || pinToVisibleBounds @@ -1300,11 +1378,10 @@ private extension MagazineLayout { return layoutAttributes } - func footerLayoutAttributes( + private func footerLayoutAttributes( for footerLocation: ElementLocation, - frame: CGRect) - -> UICollectionViewLayoutAttributes? - { + frame: CGRect + ) -> UICollectionViewLayoutAttributes? { guard footerLocation.sectionIndex < currentCollectionView.numberOfSections else { return nil } let layoutAttributes: MagazineLayoutCollectionViewLayoutAttributes @@ -1316,15 +1393,17 @@ private extension MagazineLayout { } else { layoutAttributes = MagazineLayoutCollectionViewLayoutAttributes( forSupplementaryViewOfKind: MagazineLayout.SupplementaryViewKind.sectionFooter, - with: footerLocation.indexPath) + with: footerLocation.indexPath + ) } layoutAttributes.frame = frame let sectionIndex = footerLocation.sectionIndex if - case let .visible(heightMode, pinToVisibleBounds) = visibilityModeForFooter( - inSectionAtIndex: sectionIndex) + case .visible(let heightMode, let pinToVisibleBounds) = visibilityModeForFooter( + inSectionAtIndex: sectionIndex + ) { layoutAttributes.shouldVerticallySelfSize = heightMode == .dynamic hasPinnedHeaderOrFooter = hasPinnedHeaderOrFooter || pinToVisibleBounds @@ -1338,11 +1417,10 @@ private extension MagazineLayout { return layoutAttributes } - func backgroundLayoutAttributes( + private func backgroundLayoutAttributes( for backgroundLocation: ElementLocation, - frame: CGRect) - -> UICollectionViewLayoutAttributes? - { + frame: CGRect + ) -> UICollectionViewLayoutAttributes? { guard backgroundLocation.sectionIndex < currentCollectionView.numberOfSections else { return nil } @@ -1356,7 +1434,8 @@ private extension MagazineLayout { } else { layoutAttributes = MagazineLayoutCollectionViewLayoutAttributes( forSupplementaryViewOfKind: MagazineLayout.SupplementaryViewKind.sectionBackground, - with: backgroundLocation.indexPath) + with: backgroundLocation.indexPath + ) } layoutAttributes.frame = frame @@ -1369,11 +1448,10 @@ private extension MagazineLayout { return layoutAttributes } - func itemLayoutAttributes( + private func itemLayoutAttributes( for itemLocation: ElementLocation, - frame: CGRect) - -> UICollectionViewLayoutAttributes? - { + frame: CGRect + ) -> UICollectionViewLayoutAttributes? { guard itemLocation.sectionIndex < currentCollectionView.numberOfSections else { return nil } let numberOfItems = currentCollectionView.numberOfItems(inSection: itemLocation.sectionIndex) guard itemLocation.elementIndex < numberOfItems else { return nil } @@ -1386,7 +1464,8 @@ private extension MagazineLayout { layoutAttributes = cachedLayoutAttributes } else { layoutAttributes = MagazineLayoutCollectionViewLayoutAttributes( - forCellWith: itemLocation.indexPath) + forCellWith: itemLocation.indexPath + ) } layoutAttributes.frame = frame diff --git a/MagazineLayout/Public/MagazineLayoutCollectionViewLayoutAttributes.swift b/MagazineLayout/Public/MagazineLayoutCollectionViewLayoutAttributes.swift index 833592c..a3009b6 100644 --- a/MagazineLayout/Public/MagazineLayoutCollectionViewLayoutAttributes.swift +++ b/MagazineLayout/Public/MagazineLayoutCollectionViewLayoutAttributes.swift @@ -34,7 +34,7 @@ public final class MagazineLayoutCollectionViewLayoutAttributes: UICollectionVie } override public func isEqual(_ object: Any?) -> Bool { - return super.isEqual(object) && + super.isEqual(object) && shouldVerticallySelfSize == (object as? MagazineLayoutCollectionViewLayoutAttributes)?.shouldVerticallySelfSize } diff --git a/MagazineLayout/Public/Types/MagazineLayout+Default.swift b/MagazineLayout/Public/Types/MagazineLayout+Default.swift index e137a86..6bb5e7d 100755 --- a/MagazineLayout/Public/Types/MagazineLayout+Default.swift +++ b/MagazineLayout/Public/Types/MagazineLayout+Default.swift @@ -22,7 +22,8 @@ extension MagazineLayout { public static let ItemSizeMode = MagazineLayoutItemSizeMode( widthMode: .fullWidth(respectsHorizontalInsets: true), - heightMode: MagazineLayoutItemHeightMode.static(height: ItemHeight)) + heightMode: MagazineLayoutItemHeightMode.static(height: ItemHeight) + ) public static let HeaderVisibilityMode = MagazineLayoutHeaderVisibilityMode.hidden public static let FooterVisibilityMode = MagazineLayoutFooterVisibilityMode.hidden public static let BackgroundVisibilityMode = MagazineLayoutBackgroundVisibilityMode.hidden @@ -32,8 +33,8 @@ extension MagazineLayout { public static let FooterHeight: CGFloat = 44 public static let VerticalSpacing: CGFloat = 0 public static let HorizontalSpacing: CGFloat = 0 - public static let SectionInsets: UIEdgeInsets = .zero - public static let ItemInsets: UIEdgeInsets = .zero + public static let SectionInsets = UIEdgeInsets.zero + public static let ItemInsets = UIEdgeInsets.zero } diff --git a/MagazineLayout/Public/Types/MagazineLayoutFooterVisibilityMode.swift b/MagazineLayout/Public/Types/MagazineLayoutFooterVisibilityMode.swift index a7e8edd..0e41d7c 100755 --- a/MagazineLayout/Public/Types/MagazineLayoutFooterVisibilityMode.swift +++ b/MagazineLayout/Public/Types/MagazineLayoutFooterVisibilityMode.swift @@ -30,10 +30,9 @@ public enum MagazineLayoutFooterVisibilityMode: Hashable { /// This visibility mode will cause the footer to be displayed using the specified height mode in /// its respective section. public static func visible( - heightMode: MagazineLayoutFooterHeightMode) - -> MagazineLayoutFooterVisibilityMode - { - return .visible(heightMode: heightMode, pinToVisibleBounds: false) + heightMode: MagazineLayoutFooterHeightMode + ) -> MagazineLayoutFooterVisibilityMode { + .visible(heightMode: heightMode, pinToVisibleBounds: false) } } diff --git a/MagazineLayout/Public/Types/MagazineLayoutHeaderVisibilityMode.swift b/MagazineLayout/Public/Types/MagazineLayoutHeaderVisibilityMode.swift index cb610a1..762948e 100755 --- a/MagazineLayout/Public/Types/MagazineLayoutHeaderVisibilityMode.swift +++ b/MagazineLayout/Public/Types/MagazineLayoutHeaderVisibilityMode.swift @@ -31,10 +31,9 @@ public enum MagazineLayoutHeaderVisibilityMode: Hashable { /// This visibility mode will cause the header to be displayed using the specified height mode in /// its respective section. public static func visible( - heightMode: MagazineLayoutHeaderHeightMode) - -> MagazineLayoutHeaderVisibilityMode - { - return .visible(heightMode: heightMode, pinToVisibleBounds: false) + heightMode: MagazineLayoutHeaderHeightMode + ) -> MagazineLayoutHeaderVisibilityMode { + .visible(heightMode: heightMode, pinToVisibleBounds: false) } } diff --git a/MagazineLayout/Public/Types/MagazineLayoutItemSizeMode.swift b/MagazineLayout/Public/Types/MagazineLayoutItemSizeMode.swift index d1b0dce..a4bd034 100644 --- a/MagazineLayout/Public/Types/MagazineLayoutItemSizeMode.swift +++ b/MagazineLayout/Public/Types/MagazineLayoutItemSizeMode.swift @@ -69,24 +69,26 @@ public enum MagazineLayoutItemWidthMode: Hashable { /// error and **will result in a runtime crash**. case fractionalWidth(divisor: UInt) + // MARK: Public + /// Half width items will take up `1/2` of the available width for a given row of items. public static var halfWidth: MagazineLayoutItemWidthMode { - return .fractionalWidth(divisor: 2) + .fractionalWidth(divisor: 2) } /// Third width items will take up `1/3` of the available width for a given row of items. public static var thirdWidth: MagazineLayoutItemWidthMode { - return .fractionalWidth(divisor: 3) + .fractionalWidth(divisor: 3) } /// Fourth width items will take up `1/4` of the available width for a given row of items. public static var fourthWidth: MagazineLayoutItemWidthMode { - return .fractionalWidth(divisor: 4) + .fractionalWidth(divisor: 4) } /// Fifth width items will take up `1/5` of the available width for a given row of items. public static var fifthWidth: MagazineLayoutItemWidthMode { - return .fractionalWidth(divisor: 5) + .fractionalWidth(divisor: 5) } } @@ -125,6 +127,8 @@ public enum MagazineLayoutItemHeightMode: Hashable { /// the same row of items, even if the tallest item has a `static` height mode. case dynamicAndStretchToTallestItemInRow + // MARK: Public + /// This height mode will cause the item to self-size in the vertical direction /// /// In practice, self-sizing in the vertical direction means that the item will get its height diff --git a/MagazineLayout/Public/UICollectionViewDelegateMagazineLayout.swift b/MagazineLayout/Public/UICollectionViewDelegateMagazineLayout.swift index 65637ee..e73dd58 100755 --- a/MagazineLayout/Public/UICollectionViewDelegateMagazineLayout.swift +++ b/MagazineLayout/Public/UICollectionViewDelegateMagazineLayout.swift @@ -30,8 +30,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate func collectionView( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, - sizeModeForItemAt indexPath: IndexPath) - -> MagazineLayoutItemSizeMode + sizeModeForItemAt indexPath: IndexPath + ) -> MagazineLayoutItemSizeMode /// Asks the delegate for the visibility mode of the header in the specified section. /// @@ -44,8 +44,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate func collectionView( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForHeaderInSectionAtIndex index: Int) - -> MagazineLayoutHeaderVisibilityMode + visibilityModeForHeaderInSectionAtIndex index: Int + ) -> MagazineLayoutHeaderVisibilityMode /// Asks the delegate for the visibility mode of the footer in the specified section. /// @@ -58,8 +58,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate func collectionView( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForFooterInSectionAtIndex index: Int) - -> MagazineLayoutFooterVisibilityMode + visibilityModeForFooterInSectionAtIndex index: Int + ) -> MagazineLayoutFooterVisibilityMode /// Asks the delegate for the visibility mode of the background in the specified section. /// @@ -72,8 +72,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate func collectionView( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, - visibilityModeForBackgroundInSectionAtIndex index: Int) - -> MagazineLayoutBackgroundVisibilityMode + visibilityModeForBackgroundInSectionAtIndex index: Int + ) -> MagazineLayoutBackgroundVisibilityMode /// Asks the delegate for the horizontal spacing for items in the specified section. /// @@ -86,8 +86,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate func collectionView( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, - horizontalSpacingForItemsInSectionAtIndex index: Int) - -> CGFloat + horizontalSpacingForItemsInSectionAtIndex index: Int + ) -> CGFloat /// Asks the delegate for the vertical spacing for items in the specified section. /// @@ -100,8 +100,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate func collectionView( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, - verticalSpacingForElementsInSectionAtIndex index: Int) - -> CGFloat + verticalSpacingForElementsInSectionAtIndex index: Int + ) -> CGFloat /// Asks the delegate for the amount by which to inset elements in the specified section. /// @@ -117,8 +117,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate func collectionView( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, - insetsForSectionAtIndex index: Int) - -> UIEdgeInsets + insetsForSectionAtIndex index: Int + ) -> UIEdgeInsets /// Asks the delegate for the amount by which to inset items in the specified section. /// @@ -134,8 +134,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate func collectionView( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, - insetsForItemsInSectionAtIndex index: Int) - -> UIEdgeInsets + insetsForItemsInSectionAtIndex index: Int + ) -> UIEdgeInsets /// Asks the delegate to modify a layout attributes instance so that it represents the initial visual state of an item being inserted via /// `UICollectionView.insertItems(at:)`. @@ -153,7 +153,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, initialLayoutAttributesForInsertedItemAt indexPath: IndexPath, - byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes) + byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes + ) /// Asks the delegate to modify a layout attributes instance so that it represents the initial visual state of a header being inserted /// via `UICollectionView.insertSections(_:)`. @@ -172,7 +173,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, initialLayoutAttributesForInsertedHeaderInSectionAtIndex index: Int, - byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes) + byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes + ) /// Asks the delegate to modify a layout attributes instance so that it represents the initial visual state of a footer being inserted /// via `UICollectionView.insertSections(_:)`. @@ -191,7 +193,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, initialLayoutAttributesForInsertedFooterInSectionAtIndex index: Int, - byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes) + byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes + ) /// Asks the delegate to modify a layout attributes instance so that it represents the initial visual state of a background being /// inserted via `UICollectionView.insertSections(_:)`. @@ -210,7 +213,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, initialLayoutAttributesForInsertedBackgroundInSectionAtIndex index: Int, - byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes) + byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes + ) /// Asks the delegate to modify a layout attributes instance so that it represents the final visual state of an item being removed via /// `UICollectionView.deleteItems(at:)`. @@ -229,7 +233,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, finalLayoutAttributesForRemovedItemAt indexPath: IndexPath, - byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes) + byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes + ) /// Asks the delegate to modify a layout attributes instance so that it represents the final visual state of a header being removed /// via `UICollectionView.deleteSections(_:)`. @@ -248,7 +253,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, finalLayoutAttributesForRemovedHeaderInSectionAtIndex index: Int, - byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes) + byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes + ) /// Asks the delegate to modify a layout attributes instance so that it represents the final visual state of a footer being removed /// via `UICollectionView.deleteSections(_:)`. @@ -267,7 +273,8 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, finalLayoutAttributesForRemovedFooterInSectionAtIndex index: Int, - byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes) + byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes + ) /// Asks the delegate to modify a layout attributes instance so that it represents the final visual state of a background being /// removed via `UICollectionView.deleteSections(_:)`. @@ -286,53 +293,58 @@ public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, finalLayoutAttributesForRemovedBackgroundInSectionAtIndex index: Int, - byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes) + byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes + ) } // MARK: Default Insert Animations -public extension UICollectionViewDelegateMagazineLayout { +extension UICollectionViewDelegateMagazineLayout { - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - initialLayoutAttributesForInsertedItemAt indexPath: IndexPath, - byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes) - { + // MARK: Public + + public func collectionView( + _: UICollectionView, + layout _: UICollectionViewLayout, + initialLayoutAttributesForInsertedItemAt _: IndexPath, + byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes + ) { defaultInsertAnimation(byModifying: initialLayoutAttributes) } - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - initialLayoutAttributesForInsertedHeaderInSectionAtIndex index: Int, - byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes) - { + public func collectionView( + _: UICollectionView, + layout _: UICollectionViewLayout, + initialLayoutAttributesForInsertedHeaderInSectionAtIndex _: Int, + byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes + ) { defaultInsertAnimation(byModifying: initialLayoutAttributes) } - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - initialLayoutAttributesForInsertedFooterInSectionAtIndex index: Int, - byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes) - { + public func collectionView( + _: UICollectionView, + layout _: UICollectionViewLayout, + initialLayoutAttributesForInsertedFooterInSectionAtIndex _: Int, + byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes + ) { defaultInsertAnimation(byModifying: initialLayoutAttributes) } - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - initialLayoutAttributesForInsertedBackgroundInSectionAtIndex index: Int, - byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes) - { + public func collectionView( + _: UICollectionView, + layout _: UICollectionViewLayout, + initialLayoutAttributesForInsertedBackgroundInSectionAtIndex _: Int, + byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes + ) { defaultInsertAnimation(byModifying: initialLayoutAttributes) } + // MARK: Private + private func defaultInsertAnimation( - byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes) - { + byModifying initialLayoutAttributes: UICollectionViewLayoutAttributes + ) { // The default insert animation is a simple fade-in. initialLayoutAttributes.alpha = 0 } @@ -341,47 +353,51 @@ public extension UICollectionViewDelegateMagazineLayout { // MARK: Default Delete Animations -public extension UICollectionViewDelegateMagazineLayout { +extension UICollectionViewDelegateMagazineLayout { - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - finalLayoutAttributesForRemovedItemAt indexPath: IndexPath, - byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes) - { + // MARK: Public + + public func collectionView( + _: UICollectionView, + layout _: UICollectionViewLayout, + finalLayoutAttributesForRemovedItemAt _: IndexPath, + byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes + ) { defaultDeleteAnimation(byModifying: finalLayoutAttributes) } - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - finalLayoutAttributesForRemovedHeaderInSectionAtIndex index: Int, - byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes) - { + public func collectionView( + _: UICollectionView, + layout _: UICollectionViewLayout, + finalLayoutAttributesForRemovedHeaderInSectionAtIndex _: Int, + byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes + ) { defaultDeleteAnimation(byModifying: finalLayoutAttributes) } - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - finalLayoutAttributesForRemovedFooterInSectionAtIndex index: Int, - byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes) - { + public func collectionView( + _: UICollectionView, + layout _: UICollectionViewLayout, + finalLayoutAttributesForRemovedFooterInSectionAtIndex _: Int, + byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes + ) { defaultDeleteAnimation(byModifying: finalLayoutAttributes) } - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - finalLayoutAttributesForRemovedBackgroundInSectionAtIndex index: Int, - byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes) - { + public func collectionView( + _: UICollectionView, + layout _: UICollectionViewLayout, + finalLayoutAttributesForRemovedBackgroundInSectionAtIndex _: Int, + byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes + ) { defaultDeleteAnimation(byModifying: finalLayoutAttributes) } + // MARK: Private + private func defaultDeleteAnimation( - byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes) - { + byModifying finalLayoutAttributes: UICollectionViewLayoutAttributes + ) { // The default delete animation is a simple fade-out. finalLayoutAttributes.alpha = 0 } diff --git a/MagazineLayout/Public/Views/MagazineLayoutCollectionReusableView.swift b/MagazineLayout/Public/Views/MagazineLayoutCollectionReusableView.swift index ae397b9..b955fda 100644 --- a/MagazineLayout/Public/Views/MagazineLayoutCollectionReusableView.swift +++ b/MagazineLayout/Public/Views/MagazineLayoutCollectionReusableView.swift @@ -33,9 +33,8 @@ import UIKit open class MagazineLayoutCollectionReusableView: UICollectionReusableView { override open func preferredLayoutAttributesFitting( - _ layoutAttributes: UICollectionViewLayoutAttributes) - -> UICollectionViewLayoutAttributes - { + _ layoutAttributes: UICollectionViewLayoutAttributes + ) -> UICollectionViewLayoutAttributes { guard let attributes = layoutAttributes as? MagazineLayoutCollectionViewLayoutAttributes else { assertionFailure("`layoutAttributes` must be an instance of `MagazineLayoutCollectionViewLayoutAttributes`") return super.preferredLayoutAttributesFitting(layoutAttributes) @@ -47,7 +46,8 @@ open class MagazineLayoutCollectionReusableView: UICollectionReusableView { size = super.systemLayoutSizeFitting( layoutAttributes.size, withHorizontalFittingPriority: .required, - verticalFittingPriority: .fittingSizeLevel) + verticalFittingPriority: .fittingSizeLevel + ) } else { // No self-sizing is required; respect whatever size the layout determined. size = layoutAttributes.size diff --git a/MagazineLayout/Public/Views/MagazineLayoutCollectionViewCell.swift b/MagazineLayout/Public/Views/MagazineLayoutCollectionViewCell.swift index bb2518f..cc7dc41 100644 --- a/MagazineLayout/Public/Views/MagazineLayoutCollectionViewCell.swift +++ b/MagazineLayout/Public/Views/MagazineLayoutCollectionViewCell.swift @@ -40,9 +40,8 @@ import UIKit open class MagazineLayoutCollectionViewCell: UICollectionViewCell { override open func preferredLayoutAttributesFitting( - _ layoutAttributes: UICollectionViewLayoutAttributes) - -> UICollectionViewLayoutAttributes - { + _ layoutAttributes: UICollectionViewLayoutAttributes + ) -> UICollectionViewLayoutAttributes { guard let attributes = layoutAttributes as? MagazineLayoutCollectionViewLayoutAttributes else { assertionFailure("`layoutAttributes` must be an instance of `MagazineLayoutCollectionViewLayoutAttributes`") return super.preferredLayoutAttributesFitting(layoutAttributes) @@ -74,7 +73,8 @@ open class MagazineLayoutCollectionViewCell: UICollectionViewCell { size = super.systemLayoutSizeFitting( layoutAttributes.size, withHorizontalFittingPriority: .required, - verticalFittingPriority: .fittingSizeLevel) + verticalFittingPriority: .fittingSizeLevel + ) } else { // No self-sizing is required; respect whatever size the layout determined. size = layoutAttributes.size diff --git a/Package.swift b/Package.swift index 1744c17..3fa8bfb 100644 --- a/Package.swift +++ b/Package.swift @@ -3,27 +3,27 @@ import PackageDescription let package = Package( - name: "MagazineLayout", - platforms: [ - .iOS(.v10), - .tvOS(.v10) - ], - products: [ - .library(name: "MagazineLayout", targets: ["MagazineLayout"]) - ], - dependencies: [ - .package(url: "https://github.com/airbnb/swift", .upToNextMajor(from: "1.2.0")) - ], - targets: [ - .target( - name: "MagazineLayout", - path: "MagazineLayout" - ), - .testTarget( - name: "MagazineLayoutTests", - dependencies: ["MagazineLayout"], - path: "Tests" - ) - ], - swiftLanguageVersions: [.v5] + name: "MagazineLayout", + platforms: [ + .iOS(.v10), + .tvOS(.v10), + ], + products: [ + .library(name: "MagazineLayout", targets: ["MagazineLayout"]) + ], + dependencies: [ + .package(url: "https://github.com/airbnb/swift", .upToNextMajor(from: "1.2.0")) + ], + targets: [ + .target( + name: "MagazineLayout", + path: "MagazineLayout" + ), + .testTarget( + name: "MagazineLayoutTests", + dependencies: ["MagazineLayout"], + path: "Tests" + ), + ], + swiftLanguageVersions: [.v5] ) diff --git a/Tests/ContentInsetAdjustingContentOffsetTests.swift b/Tests/ContentInsetAdjustingContentOffsetTests.swift index 8ab05d0..a2767d8 100644 --- a/Tests/ContentInsetAdjustingContentOffsetTests.swift +++ b/Tests/ContentInsetAdjustingContentOffsetTests.swift @@ -16,13 +16,16 @@ import XCTest @testable import MagazineLayout +// MARK: - ContentInsetAdjustingContentOffsetTests + final class ContentInsetAdjustingContentOffsetTests: XCTestCase { func testContentOffsetIsNotAdjustedForTopInsetChangeWithToTopBottomLayout() { let layout = MagazineLayout() let collectionView = StubCollectionView( frame: .zero, - collectionViewLayout: layout) + collectionViewLayout: layout + ) let context = MagazineLayoutInvalidationContext() layout.invalidateLayout(with: context) XCTAssertEqual(context.contentOffsetAdjustment, .zero) @@ -37,7 +40,8 @@ final class ContentInsetAdjustingContentOffsetTests: XCTestCase { layout.verticalLayoutDirection = .bottomToTop let collectionView = StubCollectionView( frame: .zero, - collectionViewLayout: layout) + collectionViewLayout: layout + ) let context = MagazineLayoutInvalidationContext() layout.invalidateLayout(with: context) XCTAssertEqual(context.contentOffsetAdjustment, .zero) @@ -52,7 +56,8 @@ final class ContentInsetAdjustingContentOffsetTests: XCTestCase { layout.verticalLayoutDirection = .bottomToTop let collectionView = StubCollectionView( frame: .zero, - collectionViewLayout: layout) + collectionViewLayout: layout + ) let context = MagazineLayoutInvalidationContext() layout.invalidateLayout(with: context) XCTAssertEqual(context.contentOffsetAdjustment, .zero) @@ -67,7 +72,8 @@ final class ContentInsetAdjustingContentOffsetTests: XCTestCase { layout.verticalLayoutDirection = .bottomToTop let collectionView = StubCollectionView( frame: .zero, - collectionViewLayout: layout) + collectionViewLayout: layout + ) let context = MagazineLayoutInvalidationContext() layout.invalidateLayout(with: context) XCTAssertEqual(context.contentOffsetAdjustment, .zero) @@ -78,9 +84,12 @@ final class ContentInsetAdjustingContentOffsetTests: XCTestCase { } } -private class StubCollectionView: UICollectionView { +// MARK: - StubCollectionView + +private final class StubCollectionView: UICollectionView { + + var stubAdjustedContentInset = UIEdgeInsets.zero - var stubAdjustedContentInset: UIEdgeInsets = .zero override var adjustedContentInset: UIEdgeInsets { stubAdjustedContentInset } diff --git a/Tests/ElementLocationFramePairsTests.swift b/Tests/ElementLocationFramePairsTests.swift index cde2f01..d6e110e 100644 --- a/Tests/ElementLocationFramePairsTests.swift +++ b/Tests/ElementLocationFramePairsTests.swift @@ -39,13 +39,15 @@ final class ElementLocationFramePairsTests: XCTestCase { func testOneElement() { let expectedDescriptions = [ - "{0, 0} & (0.0, 0.0, 100.0, 100.0)", + "{0, 0} & (0.0, 0.0, 100.0, 100.0)" ] elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 0, sectionIndex: 0), - frame: CGRect(x: 0, y: 0, width: 100, height: 100))) + frame: CGRect(x: 0, y: 0, width: 100, height: 100) + ) + ) var descriptions = [String]() for elementLocationFramePair in elementLocationFramePairs { @@ -65,11 +67,15 @@ final class ElementLocationFramePairsTests: XCTestCase { elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 0, sectionIndex: 0), - frame: CGRect(x: 0, y: 0, width: 100, height: 100))) + frame: CGRect(x: 0, y: 0, width: 100, height: 100) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 1, sectionIndex: 0), - frame: CGRect(x: 0, y: 0, width: 100, height: 100))) + frame: CGRect(x: 0, y: 0, width: 100, height: 100) + ) + ) var descriptions = [String]() for elementLocationFramePair in elementLocationFramePairs { @@ -90,46 +96,63 @@ final class ElementLocationFramePairsTests: XCTestCase { "{1, 1} & (10.0, 10.0, 200.0, 200.0)", "{1, 2} & (10.0, 10.0, 200.0, 200.0)", "{1, 3} & (10.0, 10.0, 200.0, 200.0)", - ] + ] elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 0, sectionIndex: 0), - frame: CGRect(x: 0, y: 0, width: 100, height: 100))) + frame: CGRect(x: 0, y: 0, width: 100, height: 100) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 1, sectionIndex: 0), - frame: CGRect(x: 0, y: 0, width: 100, height: 100))) + frame: CGRect(x: 0, y: 0, width: 100, height: 100) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 3, sectionIndex: 0), - frame: CGRect(x: 0, y: 0, width: 100, height: 100))) + frame: CGRect(x: 0, y: 0, width: 100, height: 100) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 4, sectionIndex: 0), - frame: CGRect(x: 0, y: 0, width: 100, height: 100))) + frame: CGRect(x: 0, y: 0, width: 100, height: 100) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 0, sectionIndex: 1), - frame: CGRect(x: 10, y: 10, width: 200, height: 200))) + frame: CGRect(x: 10, y: 10, width: 200, height: 200) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 1, sectionIndex: 1), - frame: CGRect(x: 10, y: 10, width: 200, height: 200))) + frame: CGRect(x: 10, y: 10, width: 200, height: 200) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 2, sectionIndex: 1), - frame: CGRect(x: 10, y: 10, width: 200, height: 200))) + frame: CGRect(x: 10, y: 10, width: 200, height: 200) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 3, sectionIndex: 1), - frame: CGRect(x: 10, y: 10, width: 200, height: 200))) + frame: CGRect(x: 10, y: 10, width: 200, height: 200) + ) + ) var descriptions = [String]() for elementLocationFramePair in elementLocationFramePairs { let indexPathFramePairDescription = elementLocationFramePairDescription( - from: elementLocationFramePair) + from: elementLocationFramePair + ) descriptions.append(indexPathFramePairDescription) } @@ -146,41 +169,57 @@ final class ElementLocationFramePairsTests: XCTestCase { "{1, 1} & (10.0, 10.0, 200.0, 200.0)", "{1, 2} & (10.0, 10.0, 200.0, 200.0)", "{1, 3} & (10.0, 10.0, 200.0, 200.0)", - ] + ] elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 0, sectionIndex: 0), - frame: CGRect(x: 0, y: 0, width: 100, height: 100))) + frame: CGRect(x: 0, y: 0, width: 100, height: 100) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 1, sectionIndex: 0), - frame: CGRect(x: 0, y: 0, width: 100, height: 100))) + frame: CGRect(x: 0, y: 0, width: 100, height: 100) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 3, sectionIndex: 0), - frame: CGRect(x: 0, y: 0, width: 100, height: 100))) + frame: CGRect(x: 0, y: 0, width: 100, height: 100) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 4, sectionIndex: 0), - frame: CGRect(x: 0, y: 0, width: 100, height: 100))) + frame: CGRect(x: 0, y: 0, width: 100, height: 100) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 0, sectionIndex: 1), - frame: CGRect(x: 10, y: 10, width: 200, height: 200))) + frame: CGRect(x: 10, y: 10, width: 200, height: 200) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 1, sectionIndex: 1), - frame: CGRect(x: 10, y: 10, width: 200, height: 200))) + frame: CGRect(x: 10, y: 10, width: 200, height: 200) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 2, sectionIndex: 1), - frame: CGRect(x: 10, y: 10, width: 200, height: 200))) + frame: CGRect(x: 10, y: 10, width: 200, height: 200) + ) + ) elementLocationFramePairs.append( ElementLocationFramePair( elementLocation: ElementLocation(elementIndex: 3, sectionIndex: 1), - frame: CGRect(x: 10, y: 10, width: 200, height: 200))) + frame: CGRect(x: 10, y: 10, width: 200, height: 200) + ) + ) var descriptions = [String]() for elementLocationFramePair in elementLocationFramePairs { @@ -204,9 +243,8 @@ final class ElementLocationFramePairsTests: XCTestCase { private var elementLocationFramePairs: ElementLocationFramePairs! private func elementLocationFramePairDescription( - from elementLocationFramePair: ElementLocationFramePair) - -> String - { + from elementLocationFramePair: ElementLocationFramePair + ) -> String { let sectionIndex = elementLocationFramePair.elementLocation.sectionIndex let elementIndex = elementLocationFramePair.elementLocation.elementIndex let frame = elementLocationFramePair.frame diff --git a/Tests/LayoutStateTargetContentOffsetTests.swift b/Tests/LayoutStateTargetContentOffsetTests.swift index 58e4686..b346ce4 100644 --- a/Tests/LayoutStateTargetContentOffsetTests.swift +++ b/Tests/LayoutStateTargetContentOffsetTests.swift @@ -17,18 +17,21 @@ import XCTest @testable import MagazineLayout +// MARK: - LayoutStateTargetContentOffsetTests + final class LayoutStateTargetContentOffsetTests: XCTestCase { - // MARK: Top-to-Bottom Anchor Tests + // MARK: Internal - func testAnchor_TopToBottom_ScrolledToTop() throws { + func testAnchor_TopToBottom_ScrolledToTop() { let bounds = CGRect(x: 0, y: -50, width: 300, height: 400) let layoutState = LayoutState( modelState: modelState(bounds: bounds), bounds: bounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .topToBottom) + verticalLayoutDirection: .topToBottom + ) XCTAssert(layoutState.targetContentOffsetAnchor == .top) } @@ -39,10 +42,15 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: bounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .topToBottom) + verticalLayoutDirection: .topToBottom + ) let indexPath = IndexPath(item: 6, section: 0) - let id = layoutState.modelState.idForItemModel(at: indexPath)! - XCTAssert(layoutState.targetContentOffsetAnchor == .topItem(id: id, elementLocation: ElementLocation(indexPath: indexPath), distanceFromTop: -25)) + let id = try XCTUnwrap(layoutState.modelState.idForItemModel(at: indexPath)) + XCTAssert(layoutState.targetContentOffsetAnchor == .topItem( + id: id, + elementLocation: ElementLocation(indexPath: indexPath), + distanceFromTop: -25 + )) } func testAnchor_TopToBottom_ScrolledToBottom() throws { @@ -52,7 +60,8 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: measurementBounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .topToBottom) + verticalLayoutDirection: .topToBottom + ) let maxContentOffset = measurementLayoutState.maxContentOffset let bounds = CGRect(origin: maxContentOffset, size: measurementBounds.size) @@ -61,10 +70,15 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: bounds, contentInset: measurementLayoutState.contentInset, scale: measurementLayoutState.scale, - verticalLayoutDirection: measurementLayoutState.verticalLayoutDirection) + verticalLayoutDirection: measurementLayoutState.verticalLayoutDirection + ) let indexPath = IndexPath(item: 9, section: 0) - let id = layoutState.modelState.idForItemModel(at: indexPath)! - XCTAssert(layoutState.targetContentOffsetAnchor == .topItem(id: id, elementLocation: ElementLocation(indexPath: indexPath), distanceFromTop: 25)) + let id = try XCTUnwrap(layoutState.modelState.idForItemModel(at: indexPath)) + XCTAssert(layoutState.targetContentOffsetAnchor == .topItem( + id: id, + elementLocation: ElementLocation(indexPath: indexPath), + distanceFromTop: 25 + )) } func testAnchor_TopToBottom_NoFullyVisibleCells_UsesFallback() throws { @@ -76,17 +90,20 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: bounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .topToBottom) + verticalLayoutDirection: .topToBottom + ) // Since no items are fully visible, the fallback should use the first partially visible item // instead of returning .top or .bottom let indexPath = IndexPath(item: 0, section: 0) - let id = layoutState.modelState.idForItemModel(at: indexPath)! - XCTAssert(layoutState.targetContentOffsetAnchor == .topItem(id: id, elementLocation: ElementLocation(indexPath: indexPath), distanceFromTop: -300)) + let id = try XCTUnwrap(layoutState.modelState.idForItemModel(at: indexPath)) + XCTAssert(layoutState.targetContentOffsetAnchor == .topItem( + id: id, + elementLocation: ElementLocation(indexPath: indexPath), + distanceFromTop: -300 + )) } - // MARK: Bottom-to-Top Anchor Tests - func testAnchor_BottomToTop_ScrolledToTop() throws { let bounds = CGRect(x: 0, y: -50, width: 300, height: 400) let layoutState = LayoutState( @@ -94,10 +111,15 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: bounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .bottomToTop) + verticalLayoutDirection: .bottomToTop + ) let indexPath = IndexPath(item: 3, section: 0) - let id = layoutState.modelState.idForItemModel(at: indexPath)! - XCTAssert(layoutState.targetContentOffsetAnchor == .bottomItem(id: id, elementLocation: ElementLocation(indexPath: indexPath), distanceFromBottom: -90)) + let id = try XCTUnwrap(layoutState.modelState.idForItemModel(at: indexPath)) + XCTAssert(layoutState.targetContentOffsetAnchor == .bottomItem( + id: id, + elementLocation: ElementLocation(indexPath: indexPath), + distanceFromBottom: -90 + )) } func testAnchor_BottomToTop_ScrolledToMiddle() throws { @@ -107,20 +129,26 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: bounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .bottomToTop) + verticalLayoutDirection: .bottomToTop + ) let indexPath = IndexPath(item: 10, section: 0) - let id = layoutState.modelState.idForItemModel(at: indexPath)! - XCTAssert(layoutState.targetContentOffsetAnchor == .bottomItem(id: id, elementLocation: ElementLocation(indexPath: indexPath), distanceFromBottom: -10)) + let id = try XCTUnwrap(layoutState.modelState.idForItemModel(at: indexPath)) + XCTAssert(layoutState.targetContentOffsetAnchor == .bottomItem( + id: id, + elementLocation: ElementLocation(indexPath: indexPath), + distanceFromBottom: -10 + )) } - func testAnchor_BottomToTop_ScrolledToBottom() throws { + func testAnchor_BottomToTop_ScrolledToBottom() { let measurementBounds = CGRect(x: 0, y: 0, width: 300, height: 400) let measurementLayoutState = LayoutState( modelState: modelState(bounds: measurementBounds), bounds: measurementBounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .bottomToTop) + verticalLayoutDirection: .bottomToTop + ) let maxContentOffset = measurementLayoutState.maxContentOffset let bounds = CGRect(origin: maxContentOffset, size: measurementBounds.size) @@ -129,12 +157,11 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: bounds, contentInset: measurementLayoutState.contentInset, scale: measurementLayoutState.scale, - verticalLayoutDirection: measurementLayoutState.verticalLayoutDirection) + verticalLayoutDirection: measurementLayoutState.verticalLayoutDirection + ) XCTAssert(layoutState.targetContentOffsetAnchor == .bottom) } - // MARK: Top-to-Bottom Target Content Offset Tests - func testOffset_TopToBottom_ScrolledToTop() { let bounds = CGRect(x: 0, y: -50, width: 300, height: 400) let layoutState = LayoutState( @@ -142,7 +169,8 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: bounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .topToBottom) + verticalLayoutDirection: .topToBottom + ) let targetContentOffsetAnchor = layoutState.targetContentOffsetAnchor XCTAssert(layoutState.yOffset(for: targetContentOffsetAnchor, isPerformingBatchUpdates: false) == -50) } @@ -154,7 +182,8 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: bounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .topToBottom) + verticalLayoutDirection: .topToBottom + ) let targetContentOffsetAnchor = layoutState.targetContentOffsetAnchor XCTAssert(layoutState.yOffset(for: targetContentOffsetAnchor, isPerformingBatchUpdates: false) == 500) } @@ -166,7 +195,8 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: measurementBounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .topToBottom) + verticalLayoutDirection: .topToBottom + ) let maxContentOffset = measurementLayoutState.maxContentOffset let bounds = CGRect(origin: maxContentOffset, size: measurementBounds.size) @@ -175,13 +205,12 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: bounds, contentInset: measurementLayoutState.contentInset, scale: measurementLayoutState.scale, - verticalLayoutDirection: measurementLayoutState.verticalLayoutDirection) + verticalLayoutDirection: measurementLayoutState.verticalLayoutDirection + ) let targetContentOffsetAnchor = layoutState.targetContentOffsetAnchor XCTAssert(layoutState.yOffset(for: targetContentOffsetAnchor, isPerformingBatchUpdates: false) == 690) } - // MARK: Bottom-to-Top Target Content Offset Tests - func testOffset_BottomToTop_ScrolledToTop() { let bounds = CGRect(x: 0, y: -50, width: 300, height: 400) let layoutState = LayoutState( @@ -189,7 +218,8 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: bounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .bottomToTop) + verticalLayoutDirection: .bottomToTop + ) let targetContentOffsetAnchor = layoutState.targetContentOffsetAnchor XCTAssert(layoutState.yOffset(for: targetContentOffsetAnchor, isPerformingBatchUpdates: false) == -50) } @@ -201,7 +231,8 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: bounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .bottomToTop) + verticalLayoutDirection: .bottomToTop + ) let targetContentOffsetAnchor = layoutState.targetContentOffsetAnchor XCTAssert(layoutState.yOffset(for: targetContentOffsetAnchor, isPerformingBatchUpdates: false) == 500) } @@ -213,7 +244,8 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: measurementBounds, contentInset: UIEdgeInsets(top: 50, left: 0, bottom: 30, right: 0), scale: 1, - verticalLayoutDirection: .bottomToTop) + verticalLayoutDirection: .bottomToTop + ) let maxContentOffset = measurementLayoutState.maxContentOffset let bounds = CGRect(origin: maxContentOffset, size: measurementBounds.size) @@ -222,7 +254,8 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { bounds: bounds, contentInset: measurementLayoutState.contentInset, scale: measurementLayoutState.scale, - verticalLayoutDirection: measurementLayoutState.verticalLayoutDirection) + verticalLayoutDirection: measurementLayoutState.verticalLayoutDirection + ) let targetContentOffsetAnchor = layoutState.targetContentOffsetAnchor XCTAssert(layoutState.yOffset(for: targetContentOffsetAnchor, isPerformingBatchUpdates: false) == 690) } @@ -262,7 +295,9 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { horizontalSpacing: 0, sectionInsets: .zero, itemInsets: .zero, - scale: 1)) + scale: 1 + ) + ) ] modelState.setSections(sections) return modelState @@ -289,7 +324,9 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { horizontalSpacing: 0, sectionInsets: .zero, itemInsets: .zero, - scale: 1)) + scale: 1 + ) + ) ] modelState.setSections(sections) return modelState @@ -299,16 +336,17 @@ final class LayoutStateTargetContentOffsetTests: XCTestCase { // MARK: - ItemModel -private extension ItemModel { - init( +extension ItemModel { + fileprivate init( idGenerator: IDGenerator, widthMode: MagazineLayoutItemWidthMode, - preferredHeight: CGFloat?) - { + preferredHeight: CGFloat? + ) { self.init( idGenerator: idGenerator, sizeMode: .init(widthMode: widthMode, heightMode: .dynamic), - height: 150) + height: 150 + ) self.preferredHeight = preferredHeight } } diff --git a/Tests/ModelStateEmptySectionLayoutTests.swift b/Tests/ModelStateEmptySectionLayoutTests.swift index 74f85f4..f08e29f 100644 --- a/Tests/ModelStateEmptySectionLayoutTests.swift +++ b/Tests/ModelStateEmptySectionLayoutTests.swift @@ -23,7 +23,7 @@ final class ModelStateEmptySectionLayoutTests: XCTestCase { // MARK: Internal override func setUp() { - modelState = ModelState(currentVisibleBoundsProvider: { return .zero }) + modelState = ModelState(currentVisibleBoundsProvider: { .zero }) } override func tearDown() { @@ -35,13 +35,15 @@ final class ModelStateEmptySectionLayoutTests: XCTestCase { forCollectionViewWidth: 320, sectionInsets: .zero, itemInsets: UIEdgeInsets(top: 10, left: 0, bottom: 20, right: 0), - scale: 1) + scale: 1 + ) let metrics1 = MagazineLayoutSectionMetrics.defaultSectionMetrics( forCollectionViewWidth: 320, sectionInsets: UIEdgeInsets(top: -25, left: 0, bottom: 20, right: 10), itemInsets: UIEdgeInsets(top: 50, left: 0, bottom: 100, right: 0), - scale: 1) + scale: 1 + ) let initialSections = [ SectionModel( @@ -50,23 +52,26 @@ final class ModelStateEmptySectionLayoutTests: XCTestCase { headerModel: nil, footerModel: nil, backgroundModel: nil, - metrics: metrics0), + metrics: metrics0 + ), SectionModel( idGenerator: idGenerator, itemModels: [], headerModel: nil, footerModel: nil, backgroundModel: nil, - metrics: metrics1), - ] + metrics: metrics1 + ), + ] modelState.setSections(initialSections) let expectedHeightOfSection0 = metrics0.sectionInsets.top + metrics0.sectionInsets.bottom let expectedHeightOfSection1 = metrics1.sectionInsets.top + metrics1.sectionInsets.bottom XCTAssert( - (modelState.sectionMaxY(forSectionAtIndex: 0) == expectedHeightOfSection0 && - modelState.sectionMaxY(forSectionAtIndex: 1) == expectedHeightOfSection1), - "The layout has incorrect heights for its sections") + modelState.sectionMaxY(forSectionAtIndex: 0) == expectedHeightOfSection0 && + modelState.sectionMaxY(forSectionAtIndex: 1) == expectedHeightOfSection1, + "The layout has incorrect heights for its sections" + ) } func testEmptySectionsWithHeadersFootersAndBackgroundsLayout() { @@ -74,13 +79,15 @@ final class ModelStateEmptySectionLayoutTests: XCTestCase { forCollectionViewWidth: 320, sectionInsets: UIEdgeInsets(top: 10, left: 5, bottom: 20, right: 5), itemInsets: UIEdgeInsets(top: 10, left: 10, bottom: 20, right: 10), - scale: 1) + scale: 1 + ) let metrics1 = MagazineLayoutSectionMetrics.defaultSectionMetrics( forCollectionViewWidth: 320, sectionInsets: .zero, itemInsets: UIEdgeInsets(top: 50, left: 10, bottom: 100, right: 10), - scale: 1) + scale: 1 + ) let initialSections = [ SectionModel( @@ -89,27 +96,33 @@ final class ModelStateEmptySectionLayoutTests: XCTestCase { headerModel: HeaderModel( heightMode: .static(height: 45), height: 45, - pinToVisibleBounds: false), + pinToVisibleBounds: false + ), footerModel: FooterModel( heightMode: .static(height: 45), height: 45, - pinToVisibleBounds: false), + pinToVisibleBounds: false + ), backgroundModel: BackgroundModel(), - metrics: metrics0), + metrics: metrics0 + ), SectionModel( idGenerator: idGenerator, itemModels: [], headerModel: HeaderModel( heightMode: .static(height: 65), height: 65, - pinToVisibleBounds: false), + pinToVisibleBounds: false + ), footerModel: FooterModel( heightMode: .static(height: 65), height: 65, - pinToVisibleBounds: false), + pinToVisibleBounds: false + ), backgroundModel: BackgroundModel(), - metrics: metrics1), - ] + metrics: metrics1 + ), + ] modelState.setSections(initialSections) let expectedHeaderFrames = [ @@ -120,20 +133,24 @@ final class ModelStateEmptySectionLayoutTests: XCTestCase { FrameHelpers.expectedFrames( expectedHeaderFrames, matchHeaderFramesInSectionIndexRange: 0.. [SectionModel] - { + numberOfItemsPerSection: UInt + ) -> [SectionModel] { var sectionModels = [SectionModel]() for _ in 0.. ItemModel { - return ItemModel( + ItemModel( idGenerator: idGenerator, sizeMode: MagazineLayoutItemSizeMode( widthMode: .fullWidth(respectsHorizontalInsets: true), - heightMode: .static(height: 20)), - height: 20) + heightMode: .static(height: 20) + ), + height: 20 + ) } // MARK: Private @@ -73,9 +76,8 @@ final class FrameHelpers { static func expectedFrames( _ expectedFrames: [CGRect], - match elementLocationFramePairs: ElementLocationFramePairs) - -> Bool - { + match elementLocationFramePairs: ElementLocationFramePairs + ) -> Bool { let expectedFrames = Set(expectedFrames) var checkedFramesCount = 0 @@ -93,16 +95,16 @@ final class FrameHelpers { static func expectedFrames( _ expectedFrames: [CGRect], matchItemFramesInSectionIndexRange sectionIndexRange: Range, - modelState: ModelState) - -> Bool - { + modelState: ModelState + ) -> Bool { let expectedFrames = Set(expectedFrames) var checkedFramesCount = 0 for sectionIndex in sectionIndexRange { for itemIndex in 0.., - modelState: ModelState) - -> Bool - { + modelState: ModelState + ) -> Bool { var expectedFrameIndex = 0 for sectionIndex in sectionIndexRange { let headerFrame = modelState.frameForHeader(inSectionAtIndex: sectionIndex) @@ -142,9 +143,8 @@ final class FrameHelpers { static func expectedFrames( _ expectedFrames: [CGRect?], matchFooterFramesInSectionIndexRange sectionIndexRange: Range, - modelState: ModelState) - -> Bool - { + modelState: ModelState + ) -> Bool { var expectedFrameIndex = 0 for sectionIndex in sectionIndexRange { let footerFrame = modelState.frameForFooter(inSectionAtIndex: sectionIndex) @@ -167,9 +167,8 @@ final class FrameHelpers { static func expectedFrames( _ expectedFrames: [CGRect?], matchBackgroundFramesInSectionIndexRange sectionIndexRange: Range, - modelState: ModelState) - -> Bool - { + modelState: ModelState + ) -> Bool { var expectedFrameIndex = 0 for sectionIndex in sectionIndexRange { let backgroundFrame = modelState.frameForBackground(inSectionAtIndex: sectionIndex) @@ -214,14 +213,14 @@ extension Array where Element == CGRect { // MARK: - DebugHelpers -final class DebugHelpers { +enum DebugHelpers { /// Only used while developing static func printExpectedFrameCodeToConsole( modelState: ModelState, visibleRect0: CGRect, - visibleRect1: CGRect) - { + visibleRect1: CGRect + ) { print("let expectedItemFrames0: [CGRect] = [") for pair in modelState.itemLocationFramePairs(forItemsIn: visibleRect0) { print("\tCGRect(x: \(pair.frame.minX), y: \(pair.frame.minY), width: \(pair.frame.width), height: \(pair.frame.height)),") From 36ae65f34d00b316649586ba5ea8d113ec77c56a Mon Sep 17 00:00:00 2001 From: Cal Stephens Date: Sun, 21 Dec 2025 08:32:56 -0800 Subject: [PATCH 3/3] Address lint violations --- .../contents.xcworkspacedata | 7 ++++++ MagazineLayout/LayoutCore/ModelState.swift | 2 +- .../Types/ElementLocationFramePairs.swift | 4 +-- Package.resolved | 25 +++++++++++++++++++ Tests/ElementLocationFramePairsTests.swift | 2 +- Tests/ModelStateEmptySectionLayoutTests.swift | 6 +---- Tests/ModelStateInitiallSetUpTests.swift | 6 +---- Tests/ModelStateLayoutTests.swift | 6 +---- Tests/ModelStateUpdateTests.swift | 8 ++---- Tests/TestingSupport.swift | 1 + 10 files changed, 42 insertions(+), 25 deletions(-) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 Package.resolved diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/MagazineLayout/LayoutCore/ModelState.swift b/MagazineLayout/LayoutCore/ModelState.swift index fae3fd9..f168543 100755 --- a/MagazineLayout/LayoutCore/ModelState.swift +++ b/MagazineLayout/LayoutCore/ModelState.swift @@ -230,7 +230,7 @@ final class ModelState { ) } - var itemFrame: CGRect! + var itemFrame = CGRect.zero mutateSectionModels( withUnsafeMutableBufferPointer: { directlyMutableSectionModels in itemFrame = directlyMutableSectionModels[itemLocation.sectionIndex].calculateFrameForItem( diff --git a/MagazineLayout/LayoutCore/Types/ElementLocationFramePairs.swift b/MagazineLayout/LayoutCore/Types/ElementLocationFramePairs.swift index bc93c01..74890ae 100644 --- a/MagazineLayout/LayoutCore/Types/ElementLocationFramePairs.swift +++ b/MagazineLayout/LayoutCore/Types/ElementLocationFramePairs.swift @@ -41,7 +41,7 @@ struct ElementLocationFramePairs { if first == nil { first = elementLocationFramePair } else { - last.next = elementLocationFramePair + last?.next = elementLocationFramePair } last = elementLocationFramePair @@ -53,7 +53,7 @@ struct ElementLocationFramePairs { // MARK: Private - private var last: ElementLocationFramePair! + private var last: ElementLocationFramePair? } diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..9f5513b --- /dev/null +++ b/Package.resolved @@ -0,0 +1,25 @@ +{ + "object": { + "pins": [ + { + "package": "AirbnbSwift", + "repositoryURL": "https://github.com/airbnb/swift", + "state": { + "branch": null, + "revision": "6c09dd57c2254b14bb48c15e209ed6e93058104f", + "version": "1.2.0" + } + }, + { + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser", + "state": { + "branch": null, + "revision": "c5d11a805e765f52ba34ec7284bd4fcd6ba68615", + "version": "1.7.0" + } + } + ] + }, + "version": 1 +} diff --git a/Tests/ElementLocationFramePairsTests.swift b/Tests/ElementLocationFramePairsTests.swift index d6e110e..18ea66b 100644 --- a/Tests/ElementLocationFramePairsTests.swift +++ b/Tests/ElementLocationFramePairsTests.swift @@ -240,7 +240,7 @@ final class ElementLocationFramePairsTests: XCTestCase { // MARK: Private - private var elementLocationFramePairs: ElementLocationFramePairs! + private var elementLocationFramePairs = ElementLocationFramePairs() private func elementLocationFramePairDescription( from elementLocationFramePair: ElementLocationFramePair diff --git a/Tests/ModelStateEmptySectionLayoutTests.swift b/Tests/ModelStateEmptySectionLayoutTests.swift index f08e29f..eb0fac1 100644 --- a/Tests/ModelStateEmptySectionLayoutTests.swift +++ b/Tests/ModelStateEmptySectionLayoutTests.swift @@ -26,10 +26,6 @@ final class ModelStateEmptySectionLayoutTests: XCTestCase { modelState = ModelState(currentVisibleBoundsProvider: { .zero }) } - override func tearDown() { - modelState = nil - } - func testEmptySectionsLayout() { let metrics0 = MagazineLayoutSectionMetrics.defaultSectionMetrics( forCollectionViewWidth: 320, @@ -170,6 +166,6 @@ final class ModelStateEmptySectionLayoutTests: XCTestCase { private let idGenerator = IDGenerator() - private var modelState: ModelState! + private var modelState = ModelState(currentVisibleBoundsProvider: { .zero }) } diff --git a/Tests/ModelStateInitiallSetUpTests.swift b/Tests/ModelStateInitiallSetUpTests.swift index c19b101..fdf1962 100644 --- a/Tests/ModelStateInitiallSetUpTests.swift +++ b/Tests/ModelStateInitiallSetUpTests.swift @@ -25,10 +25,6 @@ final class ModelStateInitialSetUpTests: XCTestCase { modelState = ModelState(currentVisibleBoundsProvider: { .zero }) } - override func tearDown() { - modelState = nil - } - func testInitialEmptyModelState() { XCTAssert( modelState.numberOfSections == 0, @@ -78,6 +74,6 @@ final class ModelStateInitialSetUpTests: XCTestCase { // MARK: Private - private var modelState: ModelState! + private var modelState = ModelState(currentVisibleBoundsProvider: { .zero }) } diff --git a/Tests/ModelStateLayoutTests.swift b/Tests/ModelStateLayoutTests.swift index 2a3991f..1df1ff7 100644 --- a/Tests/ModelStateLayoutTests.swift +++ b/Tests/ModelStateLayoutTests.swift @@ -59,10 +59,6 @@ final class ModelStateLayoutTests: XCTestCase { modelState.setSections(sections) } - override func tearDown() { - modelState = nil - } - func testInitialLayout() { let expectedItemFrames0: [CGRect] = [ CGRect(x: 25.0, y: 90.0, width: 280.0, height: 20.0), @@ -1302,7 +1298,7 @@ final class ModelStateLayoutTests: XCTestCase { private let idGenerator = IDGenerator() - private var modelState: ModelState! + private var modelState = ModelState(currentVisibleBoundsProvider: { .zero }) private let visibleRect0 = CGRect(x: 0, y: 0, width: 320, height: 500) private let visibleRect1 = CGRect(x: 0, y: 500, width: 320, height: 2000) diff --git a/Tests/ModelStateUpdateTests.swift b/Tests/ModelStateUpdateTests.swift index aa08f82..afd7439 100644 --- a/Tests/ModelStateUpdateTests.swift +++ b/Tests/ModelStateUpdateTests.swift @@ -25,10 +25,6 @@ final class ModelStateUpdateTests: XCTestCase { modelState = ModelState(currentVisibleBoundsProvider: { .zero }) } - override func tearDown() { - modelState = nil - } - func testIsPerformingBatchUpdates() throws { let sectionToInsert = try XCTUnwrap(ModelHelpers.basicSectionModels( numberOfSections: 1, @@ -333,7 +329,7 @@ final class ModelStateUpdateTests: XCTestCase { ) XCTAssert( - ( + try ( modelStateBeforeBatchUpdates.indexPathForItemModel( withID: try XCTUnwrap(modelStateBeforeBatchUpdates.idForItemModel(at: IndexPath(item: 0, section: 0))) ) == @@ -410,6 +406,6 @@ final class ModelStateUpdateTests: XCTestCase { // MARK: Private - private var modelState: ModelState! + private var modelState = ModelState(currentVisibleBoundsProvider: { .zero }) } diff --git a/Tests/TestingSupport.swift b/Tests/TestingSupport.swift index 6fb02d2..9aba793 100644 --- a/Tests/TestingSupport.swift +++ b/Tests/TestingSupport.swift @@ -213,6 +213,7 @@ extension Array where Element == CGRect { // MARK: - DebugHelpers +// swiftlint:disable no_direct_standard_out_logs enum DebugHelpers { /// Only used while developing