Skip to content

Commit

Permalink
Merge pull request #3 from ragingo/2022-12-22_refactor_collectionview
Browse files Browse the repository at this point in the history
refactor: CollectionView
  • Loading branch information
ragingo authored Dec 23, 2022
2 parents 9ff9306 + 70bb0a8 commit 53ad9b1
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 181 deletions.
16 changes: 12 additions & 4 deletions RagiSmoothList/RagiSmoothList.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
/* Begin PBXBuildFile section */
523DCD492951FC5100D0BA72 /* ListStyleSampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523DCD482951FC5100D0BA72 /* ListStyleSampleView.swift */; };
523DCD4E2952026A00D0BA72 /* RagiSmoothListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523DCD4C2952025F00D0BA72 /* RagiSmoothListStyle.swift */; };
523DCD5029549BDF00D0BA72 /* CustomCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523DCD4F29549BDF00D0BA72 /* CustomCollectionView.swift */; };
523DCD522954A09000D0BA72 /* CollectionViewHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523DCD512954A09000D0BA72 /* CollectionViewHolder.swift */; };
523DCD542955458700D0BA72 /* SwipeStartEdge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523DCD532955458700D0BA72 /* SwipeStartEdge.swift */; };
524ABCE62941D67800205599 /* RagiSmoothList.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D01DFC2941CE4700103B11 /* RagiSmoothList.framework */; };
524ABCE72941D67800205599 /* RagiSmoothList.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 52D01DFC2941CE4700103B11 /* RagiSmoothList.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
524ABCEC29420F0000205599 /* InnerListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524ABCEB29420F0000205599 /* InnerListCell.swift */; };
52A8FBEC294DFFC600F9C851 /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A8FBEB294DFFC600F9C851 /* DataSource.swift */; };
52A8FBEF294E004F00F9C851 /* CollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A8FBEE294E004F00F9C851 /* CollectionView.swift */; };
52D01E002941CE4700103B11 /* RagiSmoothList.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D01DFF2941CE4700103B11 /* RagiSmoothList.h */; settings = {ATTRIBUTES = (Public, ); }; };
52D01E0D2941D05900103B11 /* RagiSmoothList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D01E0C2941D05900103B11 /* RagiSmoothList.swift */; };
52D01E152941D19000103B11 /* RagiSmoothListExampleAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D01E142941D19000103B11 /* RagiSmoothListExampleAppApp.swift */; };
Expand Down Expand Up @@ -70,9 +72,11 @@
/* Begin PBXFileReference section */
523DCD482951FC5100D0BA72 /* ListStyleSampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListStyleSampleView.swift; sourceTree = "<group>"; };
523DCD4C2952025F00D0BA72 /* RagiSmoothListStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RagiSmoothListStyle.swift; sourceTree = "<group>"; };
523DCD4F29549BDF00D0BA72 /* CustomCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCollectionView.swift; sourceTree = "<group>"; };
523DCD512954A09000D0BA72 /* CollectionViewHolder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewHolder.swift; sourceTree = "<group>"; };
523DCD532955458700D0BA72 /* SwipeStartEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeStartEdge.swift; sourceTree = "<group>"; };
524ABCEB29420F0000205599 /* InnerListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerListCell.swift; sourceTree = "<group>"; };
52A8FBEB294DFFC600F9C851 /* DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSource.swift; sourceTree = "<group>"; };
52A8FBEE294E004F00F9C851 /* CollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionView.swift; sourceTree = "<group>"; };
52D01DFC2941CE4700103B11 /* RagiSmoothList.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RagiSmoothList.framework; sourceTree = BUILT_PRODUCTS_DIR; };
52D01DFF2941CE4700103B11 /* RagiSmoothList.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RagiSmoothList.h; sourceTree = "<group>"; };
52D01E0C2941D05900103B11 /* RagiSmoothList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RagiSmoothList.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -142,8 +146,10 @@
52A8FBE9294DFFAE00F9C851 /* CollectionView */ = {
isa = PBXGroup;
children = (
523DCD512954A09000D0BA72 /* CollectionViewHolder.swift */,
523DCD4F29549BDF00D0BA72 /* CustomCollectionView.swift */,
52A8FBEB294DFFC600F9C851 /* DataSource.swift */,
52A8FBEE294E004F00F9C851 /* CollectionView.swift */,
523DCD532955458700D0BA72 /* SwipeStartEdge.swift */,
);
path = CollectionView;
sourceTree = "<group>";
Expand Down Expand Up @@ -427,15 +433,17 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
523DCD5029549BDF00D0BA72 /* CustomCollectionView.swift in Sources */,
52FF044E294220E80044495F /* RagiSmoothListButtonCell.swift in Sources */,
524ABCEC29420F0000205599 /* InnerListCell.swift in Sources */,
52A8FBEF294E004F00F9C851 /* CollectionView.swift in Sources */,
52D01E0D2941D05900103B11 /* RagiSmoothList.swift in Sources */,
52A8FBEC294DFFC600F9C851 /* DataSource.swift in Sources */,
523DCD542955458700D0BA72 /* SwipeStartEdge.swift in Sources */,
52FF046029459DB20044495F /* RagiSmoothListEmptySection.swift in Sources */,
523DCD4E2952026A00D0BA72 /* RagiSmoothListStyle.swift in Sources */,
52FF04542943583F0044495F /* HostingView.swift in Sources */,
52FF045E29459D760044495F /* RagiSmoothListConfiguration.swift in Sources */,
523DCD522954A09000D0BA72 /* CollectionViewHolder.swift in Sources */,
52FF046229459E170044495F /* RagiSmoothListSectionModel.swift in Sources */,
52FF047E294AE8FD0044495F /* RagiSmoothListCellEditable.swift in Sources */,
52FF04362942149F0044495F /* InnerList.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import SwiftUI
import UIKit

final class CollectionView<
final class CollectionViewHolder<
SectionType: Hashable,
ItemType: Hashable,
SectionHeader: View,
Expand All @@ -18,17 +18,17 @@ final class CollectionView<
// swiftlint:disable:next line_length
typealias SupplementaryViewProvider = UICollectionViewDiffableDataSource<SectionType, ItemType>.SupplementaryViewProvider
typealias SwipeActionProvider = UICollectionLayoutListConfiguration.SwipeActionsConfigurationProvider
typealias DataSourceType = DataSource<SectionType, ItemType>

private(set) var dataSource: DataSource<SectionType, ItemType>
private(set) lazy var dataSource: DataSourceType = createDataSource()
private let sectionHeaderContent: (SectionType, [ItemType]) -> SectionHeader
private let sectionFooterContent: (SectionType, [ItemType]) -> SectionFooter
private let cellContent: (ItemType) -> Cell
private let onLoadMore: () -> Void
private let onRefresh: () -> Void

private let uiCollectionView: UICollectionView
private let sectionHeaderID = UUID().uuidString
private let sectionFooterID = UUID().uuidString
private var layoutListConfiguration: UICollectionLayoutListConfiguration
private let collectionView: CustomCollectionView<Cell, SectionHeader, SectionFooter>
private var layoutListConfiguration = UICollectionLayoutListConfiguration(appearance: .plain)

init(
@ViewBuilder sectionHeaderContent: @escaping (SectionType, [ItemType]) -> SectionHeader,
Expand All @@ -41,110 +41,40 @@ final class CollectionView<
self.sectionHeaderContent = sectionHeaderContent
self.sectionFooterContent = sectionFooterContent
self.cellContent = cellContent
self.onLoadMore = onLoadMore
self.onRefresh = onRefresh

let layoutListConfiguration = UICollectionLayoutListConfiguration(appearance: .plain)
self.layoutListConfiguration = layoutListConfiguration

let cellID = UUID().uuidString
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: .init())

uiCollectionView = collectionView
uiCollectionView.keyboardDismissMode = .onDragWithAccessory
uiCollectionView.register(InnerListCell<Cell>.self, forCellWithReuseIdentifier: cellID)
uiCollectionView.register(
InnerListSection<SectionHeader>.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: sectionHeaderID
)
uiCollectionView.register(
InnerListSection<SectionFooter>.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter,
withReuseIdentifier: sectionFooterID
)

self.dataSource = DataSource(
collectionView: uiCollectionView,
cellProvider: { collectionView, indexPath, item -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: cellID,
for: indexPath
) as? InnerListCell<Cell>

guard let cell else { return nil }

let isLastSection = collectionView.numberOfSections == indexPath.section + 1
let isLastItem = collectionView.numberOfItems(inSection: indexPath.section) == indexPath.row + 1
if isLastSection && isLastItem {
onLoadMore()
}

let content = cellContent(item)
cell.configure(content: content)
return cell
}
)
collectionView = .init(onRefresh: onRefresh)

self.dataSource.supplementaryViewProvider = supplementaryViewProvider()

let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(onRefreshControlValueChanged(sender:)), for: .valueChanged)
uiCollectionView.refreshControl = refreshControl

onInitialized(uiCollectionView)
}

private static func createLayout(
layoutListConfiguration: UICollectionLayoutListConfiguration
) -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { _, layoutEnvironment in
return NSCollectionLayoutSection.list(using: layoutListConfiguration, layoutEnvironment: layoutEnvironment)
}
return layout
onInitialized(collectionView)
}

func updateLayout(listStyle: any RagiSmoothListStyle, listConfiguration: RagiSmoothListConfiguration?) {
let appearance: UICollectionLayoutListConfiguration.Appearance
switch listStyle {
case is PlainListStyle:
appearance = .plain
case is GroupedListStyle:
appearance = .grouped
case is InsetListStyle:
appearance = .insetGrouped // MEMO: UICollectionView に .inset は存在しない
case is InsetGroupedListStyle:
appearance = .insetGrouped
case is SidebarListStyle:
appearance = .sidebar
case is DefaultListStyle:
appearance = .plain
default:
appearance = .plain
}
self.layoutListConfiguration = UICollectionLayoutListConfiguration(appearance: appearance)
let oldConfig = self.layoutListConfiguration

let oldConfiguration = self.layoutListConfiguration
// MEMO: もっといい方法あるかも?
self.layoutListConfiguration = .init(appearance: decideAppearance(listStyle: listStyle))
self.configureStyles(listConfiguration: listConfiguration, layoutListConfiguration: &layoutListConfiguration)
self.layoutListConfiguration.leadingSwipeActionsConfigurationProvider =
oldConfiguration.leadingSwipeActionsConfigurationProvider
oldConfig.leadingSwipeActionsConfigurationProvider
self.layoutListConfiguration.trailingSwipeActionsConfigurationProvider =
oldConfiguration.trailingSwipeActionsConfigurationProvider
Self.configureStyles(listConfiguration: listConfiguration, layoutListConfiguration: &layoutListConfiguration)
uiCollectionView.collectionViewLayout = Self.createLayout(layoutListConfiguration: layoutListConfiguration)
}
oldConfig.trailingSwipeActionsConfigurationProvider

public enum SwipeStartEdge {
case leading
case trailing
collectionView.updateLayout(layoutListConfiguration)
}

func swipeActions(
edge: SwipeStartEdge,
allowFullSwipe: Bool = true,
actions: @escaping (IndexPath) -> [UIContextualAction]
actions: @escaping (IndexPath, SectionType, ItemType) -> [UIContextualAction]
) {
let provider: SwipeActionProvider = { indexPath -> UISwipeActionsConfiguration? in
UISwipeActionsConfiguration(actions: actions(indexPath))
let provider: SwipeActionProvider = { [weak self] indexPath -> UISwipeActionsConfiguration? in
guard let self else { return nil }
let snapshot = self.dataSource.snapshot()
let section = snapshot.sectionIdentifiers[indexPath.section]
let item = snapshot.itemIdentifiers(inSection: section)[indexPath.row]
return UISwipeActionsConfiguration(actions: actions(indexPath, section, item))
}

switch edge {
Expand All @@ -155,15 +85,14 @@ final class CollectionView<
layoutListConfiguration.trailingSwipeActionsConfigurationProvider = provider
}

let layout = Self.createLayout(layoutListConfiguration: layoutListConfiguration)
uiCollectionView.collectionViewLayout = layout
collectionView.updateLayout(layoutListConfiguration)
}

func scrollToTop(animated: Bool = true) {
uiCollectionView.setContentOffset(.zero, animated: animated)
collectionView.scrollToTop(animated: animated)
}

private static func configureStyles(
private func configureStyles(
listConfiguration: RagiSmoothListConfiguration?,
layoutListConfiguration: inout UICollectionLayoutListConfiguration
) {
Expand Down Expand Up @@ -198,15 +127,51 @@ final class CollectionView<
}
}

@objc
private func onRefreshControlValueChanged(sender: UIRefreshControl) {
onRefresh()
sender.endRefreshing()
private func decideAppearance(listStyle: any RagiSmoothListStyle) -> UICollectionLayoutListConfiguration.Appearance {
let appearance: UICollectionLayoutListConfiguration.Appearance
switch listStyle {
case is PlainListStyle:
appearance = .plain
case is GroupedListStyle:
appearance = .grouped
case is InsetListStyle:
appearance = .insetGrouped // MEMO: UICollectionView に .inset は存在しない
case is InsetGroupedListStyle:
appearance = .insetGrouped
case is SidebarListStyle:
appearance = .sidebar
case is DefaultListStyle:
appearance = .plain
default:
appearance = .plain
}
return appearance
}

private func createDataSource() -> DataSourceType {
DataSource(
collectionView: collectionView,
cellProvider: { [weak self] collectionView, indexPath, item -> UICollectionViewCell? in
guard let self else { return nil }

guard let cell = self.collectionView.dequeueCell(for: indexPath) else {
return nil
}

if self.collectionView.isLastItem(indexPath) {
self.onLoadMore()
}

let content = self.cellContent(item)
cell.configure(content: content)
return cell
}
)
}
}

// MARK: - Section Header/Footer
private extension CollectionView {
private extension CollectionViewHolder {
func supplementaryViewProvider() -> SupplementaryViewProvider? {
let provider: SupplementaryViewProvider = { [weak self] _, elementKind, indexPath in
guard let self else { return nil }
Expand All @@ -223,15 +188,10 @@ private extension CollectionView {
return provider
}

func makeSectionHeader(
indexPath: IndexPath
) -> UICollectionReusableView? {
let view = uiCollectionView.dequeueReusableSupplementaryView(
ofKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: sectionHeaderID,
for: indexPath) as? InnerListSection<SectionHeader>

guard let view else { return nil }
func makeSectionHeader(indexPath: IndexPath) -> UICollectionReusableView? {
guard let view = collectionView.dequeueSectionHeader(for: indexPath) else {
return nil
}

let snapshot = dataSource.snapshot()
let sectionData = snapshot.sectionIdentifiers[indexPath.section]
Expand All @@ -242,15 +202,10 @@ private extension CollectionView {
return view
}

func makeSectionFooter(
indexPath: IndexPath
) -> UICollectionReusableView? {
let view = uiCollectionView.dequeueReusableSupplementaryView(
ofKind: UICollectionView.elementKindSectionFooter,
withReuseIdentifier: sectionFooterID,
for: indexPath) as? InnerListSection<SectionFooter>

guard let view else { return nil }
func makeSectionFooter(indexPath: IndexPath) -> UICollectionReusableView? {
guard let view = collectionView.dequeueSectionFooter(for: indexPath) else {
return nil
}

let snapshot = dataSource.snapshot()
let sectionData = snapshot.sectionIdentifiers[indexPath.section]
Expand Down
Loading

0 comments on commit 53ad9b1

Please sign in to comment.