326 lines
11 KiB
Swift
326 lines
11 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import PureLayout
|
|
import SignalServiceKit
|
|
|
|
public protocol OWSTableViewControllerDelegate: AnyObject {
|
|
func tableViewWillBeginDragging(_ tableView: UITableView)
|
|
}
|
|
|
|
open class OWSTableViewController: OWSViewController {
|
|
|
|
public weak var delegate: OWSTableViewControllerDelegate?
|
|
|
|
public var contents = OWSTableContents() {
|
|
didSet {
|
|
if oldValue !== contents {
|
|
applyContents()
|
|
}
|
|
}
|
|
}
|
|
|
|
public let tableView = UITableView(frame: .zero, style: .grouped)
|
|
|
|
public var shouldAvoidKeyboard: Bool = false
|
|
|
|
public var layoutMarginsRelativeTableContent: Bool = false
|
|
|
|
private enum Constants {
|
|
static let cellReuseIdentifier = "OWSTableCellIdentifier"
|
|
}
|
|
|
|
public override func loadView() {
|
|
super.loadView()
|
|
|
|
tableView.delegate = self
|
|
tableView.dataSource = self
|
|
tableView.tableFooterView = UIView(frame: .zero)
|
|
view.addSubview(tableView)
|
|
|
|
if shouldAvoidKeyboard {
|
|
tableView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
|
|
tableView.autoPinEdge(.bottom, to: .bottom, of: keyboardLayoutGuideView)
|
|
} else {
|
|
tableView.autoPinEdgesToSuperviewEdges()
|
|
}
|
|
|
|
tableView.register(UITableViewCell.self, forCellReuseIdentifier: Constants.cellReuseIdentifier)
|
|
|
|
configureTableViewLayoutMargins()
|
|
applyContents()
|
|
applyTheme()
|
|
}
|
|
|
|
open override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
tableView.tableFooterView = UIView(frame: .zero)
|
|
}
|
|
|
|
/// Reloads table contents when content size category changes.
|
|
///
|
|
/// Does not reload header/footer views. Subclasses that use header/footer
|
|
/// views that need to update in response to content size category changes
|
|
/// should override this method to do so manually.
|
|
public override func contentSizeCategoryDidChange() {
|
|
super.contentSizeCategoryDidChange()
|
|
|
|
// Reload when content size might need to change.
|
|
applyContents()
|
|
}
|
|
|
|
// MARK: Appearance
|
|
|
|
/// Applies theme and reloads table contents.
|
|
///
|
|
/// Does not reload header/footer views. Subclasses that use header/footer
|
|
/// views that need to update in response to theme changes should override
|
|
/// this method to do so manually.
|
|
public override func themeDidChange() {
|
|
super.themeDidChange()
|
|
|
|
applyTheme()
|
|
tableView.reloadData()
|
|
}
|
|
|
|
open func applyTheme() {
|
|
view.backgroundColor = Theme.backgroundColor
|
|
tableView.backgroundColor = Theme.backgroundColor
|
|
tableView.separatorColor = Theme.cellSeparatorColor
|
|
}
|
|
|
|
private func configureTableViewLayoutMargins() {
|
|
guard layoutMarginsRelativeTableContent else { return }
|
|
|
|
tableView.preservesSuperviewLayoutMargins = true
|
|
tableView.layoutMargins = .zero
|
|
}
|
|
|
|
private static var sectionHeaderFooterTextFont: UIFont {
|
|
return UIFont.preferredFont(forTextStyle: .caption1)
|
|
}
|
|
|
|
// MARK: Contents
|
|
|
|
private func applyContents() {
|
|
if let title = contents.title?.nilIfEmpty {
|
|
self.title = title
|
|
}
|
|
tableView.reloadData()
|
|
}
|
|
|
|
private func sectionForIndex(_ index: Int) -> OWSTableSection {
|
|
return contents.sections[index]
|
|
}
|
|
|
|
private func itemForIndexPath(_ indexPath: IndexPath) -> OWSTableItem {
|
|
return sectionForIndex(indexPath.section).items[indexPath.item]
|
|
}
|
|
|
|
// MARK: Presentation
|
|
|
|
public func present(fromViewController viewController: UIViewController) {
|
|
let navigationController = OWSNavigationController(rootViewController: self)
|
|
navigationItem.leftBarButtonItem = .systemItem(.stop) { [weak self] in
|
|
self?.dismiss(animated: true)
|
|
}
|
|
viewController.present(navigationController, animated: true)
|
|
}
|
|
}
|
|
|
|
extension OWSTableViewController: UITableViewDataSource, UITableViewDelegate {
|
|
|
|
public func numberOfSections(in tableView: UITableView) -> Int {
|
|
return contents.sections.count
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
return sectionForIndex(section).itemCount
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
let item = itemForIndexPath(indexPath)
|
|
|
|
item.tableViewController = self
|
|
|
|
if let customCell = item.getOrBuildCustomCell(tableView) {
|
|
return customCell
|
|
}
|
|
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: Constants.cellReuseIdentifier, for: indexPath)
|
|
OWSTableItem.configureCell(cell)
|
|
cell.textLabel?.text = item.title
|
|
return cell
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
if let customHeight = itemForIndexPath(indexPath).customRowHeight {
|
|
return customHeight
|
|
}
|
|
return UITableView.automaticDimension
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
|
let section = sectionForIndex(section)
|
|
|
|
if let customHeaderView = section.customHeaderView {
|
|
return customHeaderView
|
|
}
|
|
|
|
let hasPlainTextTitle = !section.headerTitle.isEmptyOrNil
|
|
let hasAttributedTextTitle = !(section.headerAttributedTitle?.string ?? "").isEmpty
|
|
if hasPlainTextTitle || hasAttributedTextTitle {
|
|
let textView = LinkingTextView()
|
|
textView.textColor = Theme.secondaryTextAndIconColor
|
|
textView.font = OWSTableViewController.sectionHeaderFooterTextFont
|
|
if hasAttributedTextTitle {
|
|
textView.attributedText = section.headerAttributedTitle
|
|
} else {
|
|
textView.text = section.headerTitle?.uppercased()
|
|
}
|
|
|
|
let sectionHeaderView = UIView()
|
|
sectionHeaderView.addSubview(textView)
|
|
textView.autoPinHeightToSuperview()
|
|
|
|
if layoutMarginsRelativeTableContent {
|
|
sectionHeaderView.preservesSuperviewLayoutMargins = true
|
|
textView.autoPinWidthToSuperviewMargins()
|
|
textView.textContainerInset = UIEdgeInsets(top: 16, leading: 0, bottom: 6, trailing: 0)
|
|
} else {
|
|
textView.autoPinWidthToSuperview()
|
|
let tableEdgeInset: CGFloat = UIDevice.current.isPlusSizePhone ? 20 : 16
|
|
textView.textContainerInset = UIEdgeInsets(top: 16, leading: tableEdgeInset, bottom: 6, trailing: tableEdgeInset)
|
|
}
|
|
|
|
return sectionHeaderView
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
|
let tableSection = sectionForIndex(section)
|
|
|
|
if let customFooterView = tableSection.customFooterView {
|
|
return customFooterView
|
|
}
|
|
|
|
let hasPlainTextTitle = !tableSection.footerTitle.isEmptyOrNil
|
|
let hasAttributedTextTitle = !(tableSection.footerAttributedTitle?.string ?? "").isEmpty
|
|
if hasPlainTextTitle || hasAttributedTextTitle {
|
|
let textView = LinkingTextView()
|
|
textView.textColor = .ows_gray45
|
|
textView.font = OWSTableViewController.sectionHeaderFooterTextFont
|
|
textView.linkTextAttributes = [
|
|
.foregroundColor: Theme.accentBlueColor,
|
|
.underlineStyle: NSUnderlineStyle(),
|
|
.font: OWSTableViewController.sectionHeaderFooterTextFont
|
|
]
|
|
if hasAttributedTextTitle {
|
|
textView.attributedText = tableSection.footerAttributedTitle
|
|
} else {
|
|
textView.text = tableSection.footerTitle
|
|
}
|
|
|
|
let sectionFooterView = UIView()
|
|
sectionFooterView.addSubview(textView)
|
|
textView.autoPinHeightToSuperview()
|
|
|
|
if layoutMarginsRelativeTableContent {
|
|
sectionFooterView.preservesSuperviewLayoutMargins = true
|
|
textView.autoPinWidthToSuperviewMargins()
|
|
textView.textContainerInset = UIEdgeInsets(top: 16, leading: 0, bottom: 6, trailing: 0)
|
|
} else {
|
|
textView.autoPinWidthToSuperview()
|
|
let tableEdgeInset: CGFloat = UIDevice.current.isPlusSizePhone ? 20 : 16
|
|
textView.textContainerInset = UIEdgeInsets(top: 16, leading: tableEdgeInset, bottom: 6, trailing: tableEdgeInset)
|
|
}
|
|
|
|
return sectionFooterView
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
|
let tableSection = sectionForIndex(section)
|
|
|
|
if let customHeight = tableSection.customHeaderHeight {
|
|
owsAssertDebug(customHeight > 0 || customHeight == UITableView.automaticDimension)
|
|
return customHeight
|
|
}
|
|
|
|
if self.tableView(tableView, viewForHeaderInSection: section) != nil {
|
|
return UITableView.automaticDimension
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
|
let tableSection = sectionForIndex(section)
|
|
|
|
if let customHeight = tableSection.customFooterHeight {
|
|
owsAssertDebug(customHeight > 0 || customHeight == UITableView.automaticDimension)
|
|
return customHeight
|
|
}
|
|
|
|
if self.tableView(tableView, viewForFooterInSection: section) != nil {
|
|
return UITableView.automaticDimension
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
|
|
let item = itemForIndexPath(indexPath)
|
|
guard item.actionBlock != nil else { return nil }
|
|
return indexPath
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
if let actionBlock = itemForIndexPath(indexPath).actionBlock {
|
|
actionBlock()
|
|
}
|
|
}
|
|
|
|
// MARK: Index
|
|
|
|
public func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
|
|
if let sectionForSectionIndexTitleBlock = contents.sectionForSectionIndexTitleBlock {
|
|
return sectionForSectionIndexTitleBlock(title, index)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
public func sectionIndexTitles(for tableView: UITableView) -> [String]? {
|
|
if let sectionIndexTitlesForTableViewBlock = contents.sectionIndexTitlesForTableViewBlock {
|
|
return sectionIndexTitlesForTableViewBlock()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MARK: Editing
|
|
|
|
public override func setEditing(_ editing: Bool, animated: Bool) {
|
|
super.setEditing(editing, animated: animated)
|
|
tableView.setEditing(editing, animated: animated)
|
|
}
|
|
|
|
public override var isEditing: Bool {
|
|
didSet {
|
|
tableView.isEditing = isEditing
|
|
}
|
|
}
|
|
}
|
|
|
|
extension OWSTableViewController: UIScrollViewDelegate {
|
|
|
|
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
delegate?.tableViewWillBeginDragging(tableView)
|
|
}
|
|
}
|