mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-07 08:18:41 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
|
|
@ -0,0 +1,738 @@
|
|||
/* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue