Dactyloidae/mobile/ios/Client/Frontend/Settings/SettingsTableViewController.swift

738 lines
28 KiB
Swift
Raw Permalink Normal View History

2026-06-26 21:04:09 -07:00
/* 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)
}
}
}
}