mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-21 20:03:08 +09:00
738 lines
28 KiB
Swift
738 lines
28 KiB
Swift
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||
|
||
import Account
|
||
import Shared
|
||
import UIKit
|
||
|
||
// A base setting class that shows a title. You probably want to subclass this, not use it directly.
|
||
class Setting: NSObject {
|
||
fileprivate var _title: NSAttributedString?
|
||
fileprivate var _footerTitle: NSAttributedString?
|
||
fileprivate var _cellHeight: CGFloat?
|
||
fileprivate var _image: UIImage?
|
||
|
||
weak var delegate: SettingsDelegate?
|
||
|
||
// The url the SettingsContentViewController will show, e.g. Licenses and Privacy Policy.
|
||
var url: URL? { return nil }
|
||
|
||
// The title shown on the pref.
|
||
var title: NSAttributedString? { return _title }
|
||
var footerTitle: NSAttributedString? { return _footerTitle }
|
||
var cellHeight: CGFloat? { return _cellHeight}
|
||
fileprivate(set) var accessibilityIdentifier: String?
|
||
|
||
// An optional second line of text shown on the pref.
|
||
var status: NSAttributedString? { return nil }
|
||
|
||
// Whether or not to show this pref.
|
||
var hidden: Bool { return false }
|
||
|
||
var style: UITableViewCellStyle { return .subtitle }
|
||
|
||
var accessoryType: UITableViewCellAccessoryType { return .none }
|
||
|
||
var textAlignment: NSTextAlignment { return .natural }
|
||
|
||
var image: UIImage? { return _image }
|
||
|
||
fileprivate(set) var enabled: Bool = true
|
||
|
||
// Called when the cell is setup. Call if you need the default behaviour.
|
||
func onConfigureCell(_ cell: UITableViewCell) {
|
||
cell.detailTextLabel?.attributedText = status
|
||
cell.detailTextLabel?.numberOfLines = 0
|
||
cell.textLabel?.attributedText = title
|
||
cell.textLabel?.textAlignment = textAlignment
|
||
cell.textLabel?.numberOfLines = 1
|
||
cell.textLabel?.lineBreakMode = .byTruncatingTail
|
||
cell.accessoryType = accessoryType
|
||
cell.accessoryView = nil
|
||
cell.selectionStyle = enabled ? .default : .none
|
||
cell.accessibilityIdentifier = accessibilityIdentifier
|
||
cell.imageView?.image = _image
|
||
if let title = title?.string {
|
||
if let detailText = cell.detailTextLabel?.text {
|
||
cell.accessibilityLabel = "\(title), \(detailText)"
|
||
} else if let status = status?.string {
|
||
cell.accessibilityLabel = "\(title), \(status)"
|
||
} else {
|
||
cell.accessibilityLabel = title
|
||
}
|
||
}
|
||
cell.accessibilityTraits = UIAccessibilityTraitButton
|
||
cell.indentationWidth = 0
|
||
cell.layoutMargins = UIEdgeInsets.zero
|
||
// So that the separator line goes all the way to the left edge.
|
||
cell.separatorInset = UIEdgeInsets.zero
|
||
}
|
||
|
||
// Called when the pref is tapped.
|
||
func onClick(_ navigationController: UINavigationController?) { return }
|
||
|
||
// Helper method to set up and push a SettingsContentViewController
|
||
func setUpAndPushSettingsContentViewController(_ navigationController: UINavigationController?) {
|
||
if let url = self.url {
|
||
let viewController = SettingsContentViewController()
|
||
viewController.settingsTitle = self.title
|
||
viewController.url = url
|
||
navigationController?.pushViewController(viewController, animated: true)
|
||
}
|
||
}
|
||
|
||
init(title: NSAttributedString? = nil, footerTitle: NSAttributedString? = nil, cellHeight: CGFloat? = nil, delegate: SettingsDelegate? = nil, enabled: Bool? = nil) {
|
||
self._title = title
|
||
self._footerTitle = footerTitle
|
||
self._cellHeight = cellHeight
|
||
self.delegate = delegate
|
||
self.enabled = enabled ?? true
|
||
}
|
||
}
|
||
|
||
// A setting in the sections panel. Contains a sublist of Settings
|
||
class SettingSection: Setting {
|
||
fileprivate let children: [Setting]
|
||
|
||
init(title: NSAttributedString? = nil, footerTitle: NSAttributedString? = nil, cellHeight: CGFloat? = nil, children: [Setting]) {
|
||
self.children = children
|
||
super.init(title: title, footerTitle: footerTitle, cellHeight: cellHeight)
|
||
}
|
||
|
||
var count: Int {
|
||
var count = 0
|
||
for setting in children where !setting.hidden {
|
||
count += 1
|
||
}
|
||
return count
|
||
}
|
||
|
||
subscript(val: Int) -> Setting? {
|
||
var i = 0
|
||
for setting in children where !setting.hidden {
|
||
if i == val {
|
||
return setting
|
||
}
|
||
i += 1
|
||
}
|
||
return nil
|
||
}
|
||
}
|
||
|
||
private class PaddedSwitch: UIView {
|
||
fileprivate static let Padding: CGFloat = 8
|
||
|
||
init(switchView: UISwitch) {
|
||
super.init(frame: CGRect.zero)
|
||
|
||
addSubview(switchView)
|
||
|
||
frame.size = CGSize(width: switchView.frame.width + PaddedSwitch.Padding, height: switchView.frame.height)
|
||
switchView.frame.origin = CGPoint(x: PaddedSwitch.Padding, y: 0)
|
||
}
|
||
|
||
required init?(coder aDecoder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
}
|
||
|
||
// A helper class for settings with a UISwitch.
|
||
// Takes and optional settingsDidChange callback and status text.
|
||
class BoolSetting: Setting {
|
||
let prefKey: String? // Sometimes a subclass will manage its own pref setting. In that case the prefkey will be nil
|
||
|
||
fileprivate let prefs: Prefs
|
||
fileprivate let defaultValue: Bool
|
||
fileprivate let settingDidChange: ((Bool) -> Void)?
|
||
fileprivate let statusText: NSAttributedString?
|
||
|
||
init(prefs: Prefs, prefKey: String? = nil, defaultValue: Bool, attributedTitleText: NSAttributedString, attributedStatusText: NSAttributedString? = nil, settingDidChange: ((Bool) -> Void)? = nil) {
|
||
self.prefs = prefs
|
||
self.prefKey = prefKey
|
||
self.defaultValue = defaultValue
|
||
self.settingDidChange = settingDidChange
|
||
self.statusText = attributedStatusText
|
||
super.init(title: attributedTitleText)
|
||
}
|
||
|
||
convenience init(prefs: Prefs, prefKey: String? = nil, defaultValue: Bool, titleText: String, statusText: String? = nil, settingDidChange: ((Bool) -> Void)? = nil) {
|
||
var statusTextAttributedString: NSAttributedString?
|
||
if let statusTextString = statusText {
|
||
statusTextAttributedString = NSAttributedString(string: statusTextString, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewHeaderTextColor])
|
||
}
|
||
self.init(prefs: prefs, prefKey: prefKey, defaultValue: defaultValue, attributedTitleText: NSAttributedString(string: titleText, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]), attributedStatusText: statusTextAttributedString, settingDidChange: settingDidChange)
|
||
}
|
||
|
||
override var status: NSAttributedString? {
|
||
return statusText
|
||
}
|
||
|
||
override func onConfigureCell(_ cell: UITableViewCell) {
|
||
super.onConfigureCell(cell)
|
||
|
||
let control = UISwitch()
|
||
control.onTintColor = UIConstants.SystemBlueColor
|
||
control.addTarget(self, action: #selector(BoolSetting.switchValueChanged(_:)), for: UIControlEvents.valueChanged)
|
||
displayBool(control)
|
||
if let title = title {
|
||
if let status = status {
|
||
control.accessibilityLabel = "\(title.string), \(status.string)"
|
||
} else {
|
||
control.accessibilityLabel = title.string
|
||
}
|
||
cell.accessibilityLabel = nil
|
||
}
|
||
cell.accessoryView = PaddedSwitch(switchView: control)
|
||
cell.selectionStyle = .none
|
||
}
|
||
|
||
@objc func switchValueChanged(_ control: UISwitch) {
|
||
writeBool(control)
|
||
settingDidChange?(control.isOn)
|
||
UnifiedTelemetry.recordEvent(category: .action, method: .change, object: .setting, value: self.prefKey, extras: ["to": control.isOn])
|
||
}
|
||
|
||
// These methods allow a subclass to control how the pref is saved
|
||
func displayBool(_ control: UISwitch) {
|
||
guard let key = prefKey else {
|
||
return
|
||
}
|
||
control.isOn = prefs.boolForKey(key) ?? defaultValue
|
||
}
|
||
|
||
func writeBool(_ control: UISwitch) {
|
||
guard let key = prefKey else {
|
||
return
|
||
}
|
||
prefs.setBool(control.isOn, forKey: key)
|
||
}
|
||
}
|
||
|
||
/// A helper class for a setting backed by a UITextField.
|
||
/// This takes an optional settingIsValid and settingDidChange callback
|
||
/// If settingIsValid returns false, the Setting will not change and the text remains red.
|
||
class StringSetting: Setting, UITextFieldDelegate {
|
||
|
||
let prefKey: String
|
||
fileprivate let Padding: CGFloat = 8
|
||
|
||
fileprivate let prefs: Prefs
|
||
fileprivate let defaultValue: String?
|
||
fileprivate let placeholder: String
|
||
fileprivate let settingDidChange: ((String?) -> Void)?
|
||
fileprivate let settingIsValid: ((String?) -> Bool)?
|
||
|
||
let textField = UITextField()
|
||
|
||
init(prefs: Prefs, prefKey: String, defaultValue: String? = nil, placeholder: String, accessibilityIdentifier: String, settingIsValid isValueValid: ((String?) -> Bool)? = nil, settingDidChange: ((String?) -> Void)? = nil) {
|
||
self.prefs = prefs
|
||
self.prefKey = prefKey
|
||
self.defaultValue = defaultValue
|
||
self.settingDidChange = settingDidChange
|
||
self.settingIsValid = isValueValid
|
||
self.placeholder = placeholder
|
||
|
||
super.init()
|
||
self.accessibilityIdentifier = accessibilityIdentifier
|
||
}
|
||
|
||
override func onConfigureCell(_ cell: UITableViewCell) {
|
||
super.onConfigureCell(cell)
|
||
if let id = accessibilityIdentifier {
|
||
textField.accessibilityIdentifier = id + "TextField"
|
||
}
|
||
textField.placeholder = placeholder
|
||
textField.textAlignment = .center
|
||
textField.delegate = self
|
||
textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
|
||
cell.isUserInteractionEnabled = true
|
||
cell.accessibilityTraits = UIAccessibilityTraitNone
|
||
cell.contentView.addSubview(textField)
|
||
|
||
textField.snp.makeConstraints { make in
|
||
make.height.equalTo(44)
|
||
make.trailing.equalTo(cell.contentView).offset(-Padding)
|
||
make.leading.equalTo(cell.contentView).offset(Padding)
|
||
}
|
||
textField.text = prefs.stringForKey(prefKey) ?? defaultValue
|
||
textFieldDidChange(textField)
|
||
}
|
||
|
||
override func onClick(_ navigationController: UINavigationController?) {
|
||
textField.becomeFirstResponder()
|
||
}
|
||
|
||
fileprivate func isValid(_ value: String?) -> Bool {
|
||
guard let test = settingIsValid else {
|
||
return true
|
||
}
|
||
return test(prepareValidValue(userInput: value))
|
||
}
|
||
|
||
/// This gives subclasses an opportunity to treat the user input string
|
||
/// before it is saved or tested.
|
||
/// Default implementation does nothing.
|
||
func prepareValidValue(userInput value: String?) -> String? {
|
||
return value
|
||
}
|
||
|
||
@objc func textFieldDidChange(_ textField: UITextField) {
|
||
let color = isValid(textField.text) ? UIConstants.TableViewRowTextColor : UIConstants.DestructiveRed
|
||
textField.textColor = color
|
||
}
|
||
|
||
@objc func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||
return isValid(textField.text)
|
||
}
|
||
|
||
@objc func textFieldDidEndEditing(_ textField: UITextField) {
|
||
let text = textField.text
|
||
if !isValid(text) {
|
||
return
|
||
}
|
||
if let text = prepareValidValue(userInput: text) {
|
||
prefs.setString(text, forKey: prefKey)
|
||
} else {
|
||
prefs.removeObjectForKey(prefKey)
|
||
}
|
||
// Call settingDidChange with text or nil.
|
||
settingDidChange?(text)
|
||
}
|
||
}
|
||
|
||
class CheckmarkSetting: Setting {
|
||
let onChanged: () -> Void
|
||
let isEnabled: () -> Bool
|
||
private let subtitle: NSAttributedString?
|
||
|
||
override var status: NSAttributedString? {
|
||
return subtitle
|
||
}
|
||
|
||
init(title: NSAttributedString, subtitle: NSAttributedString?, accessibilityIdentifier: String? = nil, isEnabled: @escaping () -> Bool, onChanged: @escaping () -> Void) {
|
||
self.subtitle = subtitle
|
||
self.onChanged = onChanged
|
||
self.isEnabled = isEnabled
|
||
super.init(title: title)
|
||
self.accessibilityIdentifier = accessibilityIdentifier
|
||
}
|
||
|
||
override func onConfigureCell(_ cell: UITableViewCell) {
|
||
super.onConfigureCell(cell)
|
||
cell.accessoryType = isEnabled() ? .checkmark : .none
|
||
cell.selectionStyle = .none
|
||
}
|
||
|
||
override func onClick(_ navigationController: UINavigationController?) {
|
||
// Force editing to end for any focused text fields so they can finish up validation first.
|
||
navigationController?.view.endEditing(true)
|
||
if !isEnabled() {
|
||
onChanged()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A helper class for a setting backed by a UITextField.
|
||
/// This takes an optional isEnabled and mandatory onClick callback
|
||
/// isEnabled is called on each tableview.reloadData. If it returns
|
||
/// false then the 'button' appears disabled.
|
||
class ButtonSetting: Setting {
|
||
let onButtonClick: (UINavigationController?) -> Void
|
||
let destructive: Bool
|
||
let isEnabled: (() -> Bool)?
|
||
|
||
init(title: NSAttributedString?, destructive: Bool = false, accessibilityIdentifier: String, isEnabled: (() -> Bool)? = nil, onClick: @escaping (UINavigationController?) -> Void) {
|
||
self.onButtonClick = onClick
|
||
self.destructive = destructive
|
||
self.isEnabled = isEnabled
|
||
super.init(title: title)
|
||
self.accessibilityIdentifier = accessibilityIdentifier
|
||
}
|
||
|
||
override func onConfigureCell(_ cell: UITableViewCell) {
|
||
super.onConfigureCell(cell)
|
||
|
||
if isEnabled?() ?? true {
|
||
cell.textLabel?.textColor = destructive ? UIConstants.DestructiveRed : UIConstants.HighlightBlue
|
||
} else {
|
||
cell.textLabel?.textColor = UIConstants.TableViewDisabledRowTextColor
|
||
}
|
||
cell.textLabel?.textAlignment = NSTextAlignment.center
|
||
cell.accessibilityTraits = UIAccessibilityTraitButton
|
||
cell.selectionStyle = .none
|
||
}
|
||
|
||
override func onClick(_ navigationController: UINavigationController?) {
|
||
// Force editing to end for any focused text fields so they can finish up validation first.
|
||
navigationController?.view.endEditing(true)
|
||
if isEnabled?() ?? true {
|
||
onButtonClick(navigationController)
|
||
}
|
||
}
|
||
}
|
||
|
||
// A helper class for prefs that deal with sync. Handles reloading the tableView data if changes to
|
||
// the fxAccount happen.
|
||
class AccountSetting: Setting, FxAContentViewControllerDelegate {
|
||
unowned var settings: SettingsTableViewController
|
||
|
||
var profile: Profile {
|
||
return settings.profile
|
||
}
|
||
|
||
override var title: NSAttributedString? { return nil }
|
||
|
||
init(settings: SettingsTableViewController) {
|
||
self.settings = settings
|
||
super.init(title: nil)
|
||
}
|
||
|
||
override func onConfigureCell(_ cell: UITableViewCell) {
|
||
super.onConfigureCell(cell)
|
||
if settings.profile.getAccount() != nil {
|
||
cell.selectionStyle = .none
|
||
}
|
||
}
|
||
|
||
override var accessoryType: UITableViewCellAccessoryType { return .none }
|
||
|
||
func contentViewControllerDidSignIn(_ viewController: FxAContentViewController, withFlags flags: FxALoginFlags) {
|
||
// This method will get called twice: once when the user signs in, and once
|
||
// when the account is verified by email – on this device or another.
|
||
// If the user hasn't dismissed the fxa content view controller,
|
||
// then we should only do that (thus finishing the sign in/verification process)
|
||
// once the account is verified.
|
||
// By the time we get to here, we should be syncing or just about to sync in the
|
||
// background, most likely from FxALoginHelper.
|
||
if flags.verified {
|
||
_ = settings.navigationController?.popToRootViewController(animated: true)
|
||
// Reload the data to reflect the new Account immediately.
|
||
settings.tableView.reloadData()
|
||
// And start advancing the Account state in the background as well.
|
||
settings.SELrefresh()
|
||
}
|
||
}
|
||
|
||
func contentViewControllerDidCancel(_ viewController: FxAContentViewController) {
|
||
NSLog("didCancel")
|
||
_ = settings.navigationController?.popToRootViewController(animated: true)
|
||
}
|
||
}
|
||
|
||
class WithAccountSetting: AccountSetting {
|
||
override var hidden: Bool { return !profile.hasAccount() }
|
||
}
|
||
|
||
class WithoutAccountSetting: AccountSetting {
|
||
override var hidden: Bool { return profile.hasAccount() }
|
||
}
|
||
|
||
@objc
|
||
protocol SettingsDelegate: class {
|
||
func settingsOpenURLInNewTab(_ url: URL)
|
||
}
|
||
|
||
// The base settings view controller.
|
||
class SettingsTableViewController: UITableViewController {
|
||
|
||
typealias SettingsGenerator = (SettingsTableViewController, SettingsDelegate?) -> [SettingSection]
|
||
|
||
fileprivate let Identifier = "CellIdentifier"
|
||
fileprivate let SectionHeaderIdentifier = "SectionHeaderIdentifier"
|
||
var settings = [SettingSection]()
|
||
|
||
weak var settingsDelegate: SettingsDelegate?
|
||
|
||
var profile: Profile!
|
||
var tabManager: TabManager!
|
||
|
||
var hasSectionSeparatorLine = true
|
||
|
||
/// Used to calculate cell heights.
|
||
fileprivate lazy var dummyToggleCell: UITableViewCell = {
|
||
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "dummyCell")
|
||
cell.accessoryView = UISwitch()
|
||
return cell
|
||
}()
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
|
||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: Identifier)
|
||
tableView.register(SettingsTableSectionHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: SectionHeaderIdentifier)
|
||
tableView.separatorColor = UIConstants.TableViewSeparatorColor
|
||
tableView.backgroundColor = UIConstants.TableViewHeaderBackgroundColor
|
||
tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 30))
|
||
tableView.estimatedRowHeight = 44
|
||
tableView.estimatedSectionHeaderHeight = 44
|
||
}
|
||
|
||
override func viewWillAppear(_ animated: Bool) {
|
||
super.viewWillAppear(animated)
|
||
|
||
settings = generateSettings()
|
||
|
||
NotificationCenter.default.addObserver(self, selector: #selector(SettingsTableViewController.SELsyncDidChangeState), name: NotificationProfileDidStartSyncing, object: nil)
|
||
NotificationCenter.default.addObserver(self, selector: #selector(SettingsTableViewController.SELsyncDidChangeState), name: NotificationProfileDidFinishSyncing, object: nil)
|
||
NotificationCenter.default.addObserver(self, selector: #selector(SettingsTableViewController.SELfirefoxAccountDidChange), name: NotificationFirefoxAccountChanged, object: nil)
|
||
|
||
tableView.reloadData()
|
||
}
|
||
|
||
override func viewDidAppear(_ animated: Bool) {
|
||
super.viewDidAppear(animated)
|
||
SELrefresh()
|
||
}
|
||
|
||
override func viewDidDisappear(_ animated: Bool) {
|
||
super.viewDidDisappear(animated)
|
||
NotificationCenter.default.removeObserver(self, name: NotificationProfileDidStartSyncing, object: nil)
|
||
NotificationCenter.default.removeObserver(self, name: NotificationProfileDidFinishSyncing, object: nil)
|
||
NotificationCenter.default.removeObserver(self, name: NotificationFirefoxAccountChanged, object: nil)
|
||
}
|
||
|
||
// Override to provide settings in subclasses
|
||
func generateSettings() -> [SettingSection] {
|
||
return []
|
||
}
|
||
|
||
@objc fileprivate func SELsyncDidChangeState() {
|
||
DispatchQueue.main.async {
|
||
self.tableView.reloadData()
|
||
}
|
||
}
|
||
|
||
@objc fileprivate func SELrefresh() {
|
||
// Through-out, be aware that modifying the control while a refresh is in progress is /not/ supported and will likely crash the app.
|
||
if let account = self.profile.getAccount() {
|
||
account.advance().upon { state in
|
||
DispatchQueue.main.async { () -> Void in
|
||
self.tableView.reloadData()
|
||
}
|
||
}
|
||
} else {
|
||
self.tableView.reloadData()
|
||
}
|
||
}
|
||
|
||
@objc func SELfirefoxAccountDidChange() {
|
||
self.tableView.reloadData()
|
||
}
|
||
|
||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||
let section = settings[indexPath.section]
|
||
if let setting = section[indexPath.row] {
|
||
var cell: UITableViewCell!
|
||
if let _ = setting.status {
|
||
// Work around http://stackoverflow.com/a/9999821 and http://stackoverflow.com/a/25901083 by using a new cell.
|
||
// I could not make any setNeedsLayout solution work in the case where we disconnect and then connect a new account.
|
||
// Be aware that dequeing and then ignoring a cell appears to cause issues; only deque a cell if you're going to return it.
|
||
cell = UITableViewCell(style: setting.style, reuseIdentifier: nil)
|
||
} else {
|
||
cell = tableView.dequeueReusableCell(withIdentifier: Identifier, for: indexPath)
|
||
}
|
||
setting.onConfigureCell(cell)
|
||
return cell
|
||
}
|
||
return tableView.dequeueReusableCell(withIdentifier: Identifier, for: indexPath)
|
||
}
|
||
|
||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||
return settings.count
|
||
}
|
||
|
||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||
let section = settings[section]
|
||
return section.count
|
||
}
|
||
|
||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SectionHeaderIdentifier) as! SettingsTableSectionHeaderFooterView
|
||
let sectionSetting = settings[section]
|
||
if let sectionTitle = sectionSetting.title?.string {
|
||
headerView.titleLabel.text = sectionTitle.uppercased()
|
||
}
|
||
// Hide the top border for the top section to avoid having a double line at the top
|
||
if section == 0 || !hasSectionSeparatorLine {
|
||
headerView.showTopBorder = false
|
||
} else {
|
||
headerView.showTopBorder = true
|
||
}
|
||
|
||
return headerView
|
||
}
|
||
|
||
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||
let sectionSetting = settings[section]
|
||
guard let sectionFooter = sectionSetting.footerTitle?.string else {
|
||
return nil
|
||
}
|
||
let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SectionHeaderIdentifier) as! SettingsTableSectionHeaderFooterView
|
||
footerView.titleLabel.text = sectionFooter
|
||
footerView.titleAlignment = .top
|
||
footerView.showBottomBorder = false
|
||
return footerView
|
||
}
|
||
|
||
// To hide a footer dynamically requires returning nil from viewForFooterInSection
|
||
// and setting the height to zero.
|
||
// However, we also want the height dynamically calculated, there is a magic constant
|
||
// for that: `UITableViewAutomaticDimension`.
|
||
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
||
let sectionSetting = settings[section]
|
||
if let _ = sectionSetting.footerTitle?.string {
|
||
return UITableViewAutomaticDimension
|
||
}
|
||
return 0
|
||
}
|
||
|
||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||
let section = settings[indexPath.section]
|
||
// Workaround for calculating the height of default UITableViewCell cells with a subtitle under
|
||
// the title text label.
|
||
if let setting = section[indexPath.row], setting is BoolSetting && setting.status != nil {
|
||
return calculateStatusCellHeightForSetting(setting)
|
||
}
|
||
if let setting = section[indexPath.row], let height = setting.cellHeight {
|
||
return height
|
||
}
|
||
|
||
return UITableViewAutomaticDimension
|
||
}
|
||
|
||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||
let section = settings[indexPath.section]
|
||
if let setting = section[indexPath.row], setting.enabled {
|
||
setting.onClick(navigationController)
|
||
}
|
||
}
|
||
|
||
fileprivate func calculateStatusCellHeightForSetting(_ setting: Setting) -> CGFloat {
|
||
dummyToggleCell.layoutSubviews()
|
||
|
||
let topBottomMargin: CGFloat = 10
|
||
let width = dummyToggleCell.contentView.frame.width - 2 * dummyToggleCell.separatorInset.left
|
||
|
||
return
|
||
heightForLabel(dummyToggleCell.textLabel!, width: width, text: setting.title?.string) +
|
||
heightForLabel(dummyToggleCell.detailTextLabel!, width: width, text: setting.status?.string) +
|
||
2 * topBottomMargin
|
||
}
|
||
|
||
fileprivate func heightForLabel(_ label: UILabel, width: CGFloat, text: String?) -> CGFloat {
|
||
guard let text = text else { return 0 }
|
||
|
||
let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
|
||
let attrs = [NSFontAttributeName: label.font as Any]
|
||
let boundingRect = NSString(string: text).boundingRect(with: size,
|
||
options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: attrs, context: nil)
|
||
return boundingRect.height
|
||
}
|
||
}
|
||
|
||
struct SettingsTableSectionHeaderFooterViewUX {
|
||
static let titleHorizontalPadding: CGFloat = 15
|
||
static let titleVerticalPadding: CGFloat = 6
|
||
static let titleVerticalLongPadding: CGFloat = 20
|
||
}
|
||
|
||
class SettingsTableSectionHeaderFooterView: UITableViewHeaderFooterView {
|
||
|
||
enum TitleAlignment {
|
||
case top
|
||
case bottom
|
||
}
|
||
|
||
var titleAlignment: TitleAlignment = .bottom {
|
||
didSet {
|
||
remakeTitleAlignmentConstraints()
|
||
}
|
||
}
|
||
|
||
var showTopBorder: Bool = true {
|
||
didSet {
|
||
topBorder.isHidden = !showTopBorder
|
||
}
|
||
}
|
||
|
||
var showBottomBorder: Bool = true {
|
||
didSet {
|
||
bottomBorder.isHidden = !showBottomBorder
|
||
}
|
||
}
|
||
|
||
lazy var titleLabel: UILabel = {
|
||
var headerLabel = UILabel()
|
||
headerLabel.textColor = UIConstants.TableViewHeaderTextColor
|
||
headerLabel.font = UIFont.systemFont(ofSize: 12.0, weight: UIFontWeightRegular)
|
||
headerLabel.numberOfLines = 0
|
||
return headerLabel
|
||
}()
|
||
|
||
fileprivate lazy var topBorder: UIView = {
|
||
let topBorder = UIView()
|
||
topBorder.backgroundColor = UIConstants.SeparatorColor
|
||
return topBorder
|
||
}()
|
||
|
||
fileprivate lazy var bottomBorder: UIView = {
|
||
let bottomBorder = UIView()
|
||
bottomBorder.backgroundColor = UIConstants.SeparatorColor
|
||
return bottomBorder
|
||
}()
|
||
|
||
override init(reuseIdentifier: String?) {
|
||
super.init(reuseIdentifier: reuseIdentifier)
|
||
contentView.backgroundColor = UIConstants.TableViewHeaderBackgroundColor
|
||
addSubview(titleLabel)
|
||
addSubview(topBorder)
|
||
addSubview(bottomBorder)
|
||
|
||
setupInitialConstraints()
|
||
}
|
||
|
||
required init?(coder aDecoder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
func setupInitialConstraints() {
|
||
bottomBorder.snp.makeConstraints { make in
|
||
make.bottom.left.right.equalTo(self)
|
||
make.height.equalTo(0.5)
|
||
}
|
||
|
||
topBorder.snp.makeConstraints { make in
|
||
make.top.left.right.equalTo(self)
|
||
make.height.equalTo(0.5)
|
||
}
|
||
|
||
remakeTitleAlignmentConstraints()
|
||
}
|
||
|
||
override func prepareForReuse() {
|
||
super.prepareForReuse()
|
||
showTopBorder = true
|
||
showBottomBorder = true
|
||
titleLabel.text = nil
|
||
titleAlignment = .bottom
|
||
}
|
||
|
||
fileprivate func remakeTitleAlignmentConstraints() {
|
||
switch titleAlignment {
|
||
case .top:
|
||
titleLabel.snp.remakeConstraints { make in
|
||
make.left.right.equalTo(self).inset(SettingsTableSectionHeaderFooterViewUX.titleHorizontalPadding)
|
||
make.top.equalTo(self).offset(SettingsTableSectionHeaderFooterViewUX.titleVerticalPadding)
|
||
make.bottom.equalTo(self).offset(-SettingsTableSectionHeaderFooterViewUX.titleVerticalLongPadding)
|
||
}
|
||
case .bottom:
|
||
titleLabel.snp.remakeConstraints { make in
|
||
make.left.right.equalTo(self).inset(SettingsTableSectionHeaderFooterViewUX.titleHorizontalPadding)
|
||
make.bottom.equalTo(self).offset(-SettingsTableSectionHeaderFooterViewUX.titleVerticalPadding)
|
||
make.top.equalTo(self).offset(SettingsTableSectionHeaderFooterViewUX.titleVerticalLongPadding)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|