Skip to content

Commit

Permalink
Add support for headers/footers in ASTableView (apptekstudios#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
apptekstudios authored Nov 11, 2019
1 parent 4a758bb commit 4f33e4b
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 32 deletions.
20 changes: 19 additions & 1 deletion Demo/ASCollectionViewDemo/Models/Post.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,25 @@ struct Post: Identifiable

struct DataSource
{
static func postsForSection(_ sectionID: Int, number: Int = 12) -> [Post]
static func postsForGridSection(_ sectionID: Int, number: Int = 12) -> [Post]
{
(0..<number).map
{ b -> Post in
let aspect: CGFloat = 1
return Post.randomPost(sectionID * 10_000 + b, aspectRatio: aspect, offset: b)
}
}

static func postsForInstaSection(_ sectionID: Int, number: Int = 12) -> [Post]
{
(0..<number).map
{ b -> Post in
let aspect: CGFloat = [0.75, 1.0, 1.5].randomElement() ?? 1
return Post.randomPost(sectionID * 10_000 + b, aspectRatio: aspect, offset: b)
}
}

static func postsForWaterfallSection(_ sectionID: Int, number: Int = 12) -> [Post]
{
(0..<number).map
{ b -> Post in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct AdjustableGridScreen: View
@ObservedObject var layoutState = LayoutState()
@State var showConfig: Bool = true
@State var animateChange: Bool = false
@State var data: [Post] = DataSource.postsForSection(1, number: 1000)
@State var data: [Post] = DataSource.postsForGridSection(1, number: 1000)

typealias SectionID = Int

Expand Down
17 changes: 14 additions & 3 deletions Demo/ASCollectionViewDemo/Screens/InstaFeed/InstaFeedScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import UIKit

struct InstaFeedScreen: View
{
@State var data: [[Post]] = (0...3).map { DataSource.postsForSection($0) }
@State var data: [[Post]] = (0...3).map { DataSource.postsForInstaSection($0) }

var sections: [ASTableViewSection<Int>]
{
Expand Down Expand Up @@ -37,11 +37,22 @@ struct InstaFeedScreen: View
return ASTableViewSection(
id: i,
data: sectionData,
estimatedItemSize: CGSize(width: 0, height: 500),
onCellEvent: onCellEventPosts)
{ item, _ in
PostView(post: item)
}
.tableViewSetEstimatedSizes(rowHeight: 500, headerHeight: 50) //Optional: Provide reasonable estimated heights for this section
.sectionHeader {
VStack(spacing: 0) {
HStack {
Text("Demo sticky header view")
.padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20))
Spacer()
}
Divider()
}
.background(Color(.secondarySystemBackground))
}
}
}
}
Expand All @@ -60,7 +71,7 @@ struct InstaFeedScreen: View
func loadMoreContent()
{
let a = data.count
data.append(DataSource.postsForSection(a))
data.append(DataSource.postsForInstaSection(a))
}

func onCellEventStories(_ event: CellEvent<Post>)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ struct MagazineLayoutScreen: View
{
@State var data: [[Post]] = (0...5).map
{
DataSource.postsForSection($0, number: 10)
DataSource.postsForGridSection($0, number: 10)
}

var sections: [ASCollectionViewSection<Int>]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import UIKit

struct PhotoGridScreen: View
{
@State var data: [Post] = DataSource.postsForSection(1, number: 1000)
@State var data: [Post] = DataSource.postsForGridSection(1, number: 1000)
@State var selectedItems: IndexSet = []

@Environment(\.editMode) private var editMode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import UIKit
///THIS IS A WORK IN PROGRESS
struct WaterfallScreen: View
{
@State var data: [Post] = DataSource.postsForSection(1, number: 1000)
@State var data: [Post] = DataSource.postsForWaterfallSection(1, number: 1000)
@State var selectedItems: [SectionID: IndexSet] = [:]
@State var columnMinSize: CGFloat = 150

Expand Down
21 changes: 14 additions & 7 deletions Sources/ASCollectionView/ASCollectionViewSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ public struct ASCollectionViewSection<SectionID: Hashable>: Hashable
dataSource.getUniqueItemIDs(withSectionID: id)
}

var estimatedItemSize: CGSize?
//Only relevant for ASTableView
var estimatedRowHeight: CGFloat?
var estimatedHeaderHeight: CGFloat?
var estimatedFooterHeight: CGFloat?

/**
Initializes a section with data
Expand All @@ -42,7 +45,6 @@ public struct ASCollectionViewSection<SectionID: Hashable>: Hashable
- id: The id for this section
- data: The data to display in the section. This initialiser expects data that conforms to 'Identifiable'
- dataID: The keypath to a hashable identifier of each data item
- estimatedItemSize: (Optional) Provide an estimated item size to aid in calculating the layout
- onCellEvent: Use this to respond to cell appearance/disappearance, and preloading events.
- onDragDropEvent: Define this closure to enable drag/drop and respond to events (default is nil: drag/drop disabled)
- contentBuilder: A closure returning a SwiftUI view for the given data item
Expand All @@ -51,13 +53,11 @@ public struct ASCollectionViewSection<SectionID: Hashable>: Hashable
id: SectionID,
data: [Data],
dataID dataIDKeyPath: KeyPath<Data, DataID>,
estimatedItemSize: CGSize? = nil,
onCellEvent: OnCellEvent<Data>? = nil,
onDragDropEvent: OnDragDrop<Data>? = nil,
@ViewBuilder contentBuilder: @escaping ((Data, CellContext) -> Content))
{
self.id = id
self.estimatedItemSize = estimatedItemSize
dataSource = ASSectionDataSource<Data, DataID, Content>(
data: data,
dataIDKeyPath: dataIDKeyPath,
Expand Down Expand Up @@ -137,6 +137,15 @@ public extension ASCollectionViewSection
section.setSupplementaryView(content(), ofKind: kind)
return section
}

func tableViewSetEstimatedSizes(rowHeight: CGFloat? = nil, headerHeight: CGFloat? = nil, footerHeight: CGFloat? = nil) -> Self
{
var section = self
section.estimatedRowHeight = rowHeight
section.estimatedHeaderHeight = headerHeight
section.estimatedFooterHeight = footerHeight
return section
}
}

// MARK: STATIC CONTENT SECTION
Expand Down Expand Up @@ -189,19 +198,17 @@ public extension ASCollectionViewSection
- Parameters:
- id: The id for this section
- data: The data to display in the section. This initialiser expects data that conforms to 'Identifiable'
- estimatedItemSize: (Optional) Provide an estimated item size to aid in calculating the layout
- onCellEvent: Use this to respond to cell appearance/disappearance, and preloading events.
- onDragDropEvent: Define this closure to enable drag/drop and respond to events (default is nil: drag/drop disabled)
- contentBuilder: A closure returning a SwiftUI view for the given data item
*/
@inlinable init<Content: View, Data: Identifiable>(
id: SectionID,
data: [Data],
estimatedItemSize: CGSize? = nil,
onCellEvent: OnCellEvent<Data>? = nil,
onDragDropEvent: OnDragDrop<Data>? = nil,
@ViewBuilder contentBuilder: @escaping ((Data, CellContext) -> Content))
{
self.init(id: id, data: data, dataID: \.id, estimatedItemSize: estimatedItemSize, onCellEvent: onCellEvent, onDragDropEvent: onDragDropEvent, contentBuilder: contentBuilder)
self.init(id: id, data: data, dataID: \.id, onCellEvent: onCellEvent, onDragDropEvent: onDragDropEvent, contentBuilder: contentBuilder)
}
}
93 changes: 81 additions & 12 deletions Sources/ASCollectionView/ASTableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ extension ASTableView where SectionID == Int
- Parameters:
- section: A single section (ASTableViewSection)
*/
public init(mode: UITableView.Style = .plain, selectedItems: Binding<IndexSet>? = nil, section: Section)
public init(style: UITableView.Style = .plain, selectedItems: Binding<IndexSet>? = nil, section: Section)
{
self.mode = mode
self.style = style
self.selectedItems = selectedItems.map
{ selectedItems in
Binding(
Expand All @@ -27,13 +27,13 @@ extension ASTableView where SectionID == Int
Initializes a table view with a single section.
*/
public init<Data, DataID: Hashable, Content: View>(
mode: UITableView.Style = .plain,
style: UITableView.Style = .plain,
data: [Data],
dataID dataIDKeyPath: KeyPath<Data, DataID>,
selectedItems: Binding<IndexSet>? = nil,
@ViewBuilder contentBuilder: @escaping ((Data, CellContext) -> Content))
{
self.mode = mode
self.style = style
let section = ASTableViewSection(
id: 0,
data: data,
Expand All @@ -53,7 +53,7 @@ extension ASTableView where SectionID == Int
*/
init(@ViewArrayBuilder staticContent: (() -> [AnyView])) //Clashing with above functions in Swift 5.1, therefore internal for time being
{
self.mode = .plain
self.style = .plain
self.sections = [
ASTableViewSection(id: 0, content: staticContent)
]
Expand All @@ -66,7 +66,7 @@ public struct ASTableView<SectionID: Hashable>: UIViewControllerRepresentable
{
public typealias Section = ASTableViewSection<SectionID>
public var sections: [Section]
public var mode: UITableView.Style
public var style: UITableView.Style
public var selectedItems: Binding<[SectionID: IndexSet]>?

@Environment(\.tableViewSeparatorsEnabled) private var separatorsEnabled
Expand All @@ -82,16 +82,16 @@ public struct ASTableView<SectionID: Hashable>: UIViewControllerRepresentable
- Parameters:
- sections: An array of sections (ASTableViewSection)
*/
@inlinable public init(mode: UITableView.Style = .plain, selectedItems: Binding<[SectionID: IndexSet]>? = nil, sections: [Section])
@inlinable public init(style: UITableView.Style = .plain, selectedItems: Binding<[SectionID: IndexSet]>? = nil, sections: [Section])
{
self.mode = mode
self.style = style
self.selectedItems = selectedItems
self.sections = sections
}

@inlinable public init(mode: UITableView.Style = .plain, selectedItems: Binding<[SectionID: IndexSet]>? = nil, @SectionArrayBuilder <SectionID> sectionBuilder: () -> [Section])
@inlinable public init(style: UITableView.Style = .plain, selectedItems: Binding<[SectionID: IndexSet]>? = nil, @SectionArrayBuilder <SectionID> sectionBuilder: () -> [Section])
{
self.mode = mode
self.style = style
self.selectedItems = selectedItems
sections = sectionBuilder()
}
Expand All @@ -100,7 +100,7 @@ public struct ASTableView<SectionID: Hashable>: UIViewControllerRepresentable
{
context.coordinator.parent = self

let tableViewController = UITableViewController(style: .plain)
let tableViewController = UITableViewController(style: style)
tableViewController.tableView.tableFooterView = UIView()
updateTableViewSettings(tableViewController.tableView)
context.coordinator.tableViewController = tableViewController
Expand Down Expand Up @@ -246,7 +246,7 @@ public struct ASTableView<SectionID: Hashable>: UIViewControllerRepresentable

public func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
{
parent.sections[indexPath.section].estimatedItemSize?.height ?? 50
parent.sections[indexPath.section].estimatedRowHeight ?? 50
}

public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
Expand Down Expand Up @@ -336,6 +336,58 @@ public struct ASTableView<SectionID: Hashable>: UIViewControllerRepresentable
selectedItemsBinding.wrappedValue = selectedBySection
}
}

public func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
guard self.parent.sections[section].supplementary(ofKind: UICollectionView.elementKindSectionHeader) != nil else {
return CGFloat.leastNormalMagnitude
}
return parent.sections[section].estimatedHeaderHeight ?? 50
}

public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard self.parent.sections[section].supplementary(ofKind: UICollectionView.elementKindSectionHeader) != nil else {
return CGFloat.leastNormalMagnitude
}
return UITableView.automaticDimension
}

public func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat {
guard self.parent.sections[section].supplementary(ofKind: UICollectionView.elementKindSectionFooter) != nil else {
return CGFloat.leastNormalMagnitude
}
return parent.sections[section].estimatedFooterHeight ?? 50
}

public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
guard self.parent.sections[section].supplementary(ofKind: UICollectionView.elementKindSectionFooter) != nil else {
return CGFloat.leastNormalMagnitude
}
return UITableView.automaticDimension
}

public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let reusableView = tableView.dequeueReusableHeaderFooterView(withIdentifier: self.supplementaryReuseID) as? ASTableViewSupplementaryView
else { return nil }
if let supplementaryView = self.parent.sections[section].supplementary(ofKind: UICollectionView.elementKindSectionHeader)
{
reusableView.setupFor(
id: section,
view: supplementaryView)
}
return reusableView
}

public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
guard let reusableView = tableView.dequeueReusableHeaderFooterView(withIdentifier: self.supplementaryReuseID) as? ASTableViewSupplementaryView
else { return nil }
if let supplementaryView = self.parent.sections[section].supplementary(ofKind: UICollectionView.elementKindSectionFooter)
{
reusableView.setupFor(
id: section,
view: supplementaryView)
}
return reusableView
}

public func scrollViewDidScroll(_ scrollView: UIScrollView)
{
Expand All @@ -360,3 +412,20 @@ public struct ASTableView<SectionID: Hashable>: UIViewControllerRepresentable
}
}
}

/*
class ASTableViewDataSource<SectionIdentifierType, ItemIdentifierType>: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable {
public typealias HeaderFooterViewProvider = (_ sectionIndex: Int) -> UITableViewHeaderFooterView?

public var headerViewProvider: HeaderFooterViewProvider?
public var footerViewProvider: HeaderFooterViewProvider?

func headerView(forSection section: Int) -> UITableViewHeaderFooterView? {
headerViewProvider?(section)
}

func footerView(forSection section: Int) -> UITableViewHeaderFooterView? {
footerViewProvider?(section)
}
}
*/
19 changes: 14 additions & 5 deletions Sources/ASCollectionView/ASTableViewCells.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,16 @@ class ASTableViewSupplementaryView: UITableViewHeaderFooterView
var hostingController: ASHostingControllerProtocol?

private(set) var id: Int?


override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
backgroundView = UIView()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func setupFor<Content: View>(id: Int, view: Content?)
{
self.id = id
Expand All @@ -99,7 +108,7 @@ class ASTableViewSupplementaryView: UITableViewHeaderFooterView
else
{
hostingController = nil
subviews.forEach { $0.removeFromSuperview() }
contentView.subviews.forEach { $0.removeFromSuperview() }
}
}

Expand All @@ -115,7 +124,7 @@ class ASTableViewSupplementaryView: UITableViewHeaderFooterView
{
$0.viewController.removeFromParent()
vc?.addChild($0.viewController)
addSubview($0.viewController.view)
contentView.addSubview($0.viewController.view)

setNeedsLayout()

Expand All @@ -131,13 +140,13 @@ class ASTableViewSupplementaryView: UITableViewHeaderFooterView
override func prepareForReuse()
{
hostingController = nil
subviews.forEach { $0.removeFromSuperview() }
contentView.subviews.forEach { $0.removeFromSuperview() }
}

override func layoutSubviews()
{
super.layoutSubviews()
hostingController?.viewController.view.frame = bounds
hostingController?.viewController.view.frame = contentView.bounds
}

override func systemLayoutSizeFitting(_ targetSize: CGSize) -> CGSize
Expand Down

0 comments on commit 4f33e4b

Please sign in to comment.