TM-SGNL-iOS/SignalUI/ViewControllers/OWSTableViewController.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

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)
}
}