Dactyloidae/mobile/ios/Client/Frontend/Widgets/TwoLineCell.swift
2026-06-26 21:04:09 -07:00

263 lines
9.6 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 UIKit
struct TwoLineCellUX {
static let ImageSize: CGFloat = 29
static let ImageCornerRadius: CGFloat = 8
static let BorderViewMargin: CGFloat = 16
static let BadgeSize: CGFloat = 16
static let BadgeMargin: CGFloat = 16
static let BorderFrameSize: CGFloat = 32
static let TextColor = UIAccessibilityDarkerSystemColorsEnabled() ? UIColor.black : UIColor(rgb: 0x333333)
static let DetailTextColor = UIAccessibilityDarkerSystemColorsEnabled() ? UIColor.darkGray : UIColor.gray
static let DetailTextTopMargin: CGFloat = 0
}
class TwoLineTableViewCell: UITableViewCell {
fileprivate let twoLineHelper = TwoLineCellHelper()
let _textLabel = UILabel()
let _detailTextLabel = UILabel()
// Override the default labels with our own to disable default UITableViewCell label behaviours like dynamic type
override var textLabel: UILabel? {
return _textLabel
}
override var detailTextLabel: UILabel? {
return _detailTextLabel
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: UITableViewCellStyle.subtitle, reuseIdentifier: reuseIdentifier)
contentView.addSubview(_textLabel)
contentView.addSubview(_detailTextLabel)
twoLineHelper.setUpViews(self, textLabel: textLabel!, detailTextLabel: detailTextLabel!, imageView: imageView!)
indentationWidth = 0
layoutMargins = UIEdgeInsets.zero
separatorInset = UIEdgeInsets(top: 0, left: TwoLineCellUX.ImageSize + 2 * TwoLineCellUX.BorderViewMargin, bottom: 0, right: 0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
twoLineHelper.layoutSubviews()
}
override func prepareForReuse() {
super.prepareForReuse()
self.textLabel!.alpha = 1
self.imageView!.alpha = 1
self.selectionStyle = .default
separatorInset = UIEdgeInsets(top: 0, left: TwoLineCellUX.ImageSize + 2 * TwoLineCellUX.BorderViewMargin, bottom: 0, right: 0)
twoLineHelper.setupDynamicFonts()
}
// Save background color on UITableViewCell "select" because it disappears in the default behavior
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
let color = imageView?.backgroundColor
super.setHighlighted(highlighted, animated: animated)
imageView?.backgroundColor = color
}
// Save background color on UITableViewCell "select" because it disappears in the default behavior
override func setSelected(_ selected: Bool, animated: Bool) {
let color = imageView?.backgroundColor
super.setSelected(selected, animated: animated)
imageView?.backgroundColor = color
}
func setRightBadge(_ badge: UIImage?) {
if let badge = badge {
self.accessoryView = UIImageView(image: badge)
} else {
self.accessoryView = nil
}
twoLineHelper.hasRightBadge = badge != nil
}
func setLines(_ text: String?, detailText: String?) {
twoLineHelper.setLines(text, detailText: detailText)
}
func mergeAccessibilityLabels(_ views: [AnyObject?]? = nil) {
twoLineHelper.mergeAccessibilityLabels(views)
}
}
class SiteTableViewCell: TwoLineTableViewCell {
let borderView = UIView()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: UITableViewCellStyle.subtitle, reuseIdentifier: reuseIdentifier)
twoLineHelper.setUpViews(self, textLabel: textLabel!, detailTextLabel: detailTextLabel!, imageView: imageView!)
}
override func layoutSubviews() {
super.layoutSubviews()
twoLineHelper.layoutSubviews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TwoLineHeaderFooterView: UITableViewHeaderFooterView {
fileprivate let twoLineHelper = TwoLineCellHelper()
// UITableViewHeaderFooterView includes textLabel and detailTextLabel, so we can't override
// them. Unfortunately, they're also used in ways that interfere with us just using them: I get
// hard crashes in layout if I just use them; it seems there's a battle over adding to the
// contentView. So we add our own members, and cover up the other ones.
let _textLabel = UILabel()
let _detailTextLabel = UILabel()
let imageView = UIImageView()
// Yes, this is strange.
override var textLabel: UILabel? {
return _textLabel
}
// Yes, this is strange.
override var detailTextLabel: UILabel? {
return _detailTextLabel
}
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
twoLineHelper.setUpViews(self, textLabel: _textLabel, detailTextLabel: _detailTextLabel, imageView: imageView)
contentView.addSubview(_textLabel)
contentView.addSubview(_detailTextLabel)
contentView.addSubview(imageView)
layoutMargins = UIEdgeInsets.zero
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
twoLineHelper.layoutSubviews()
}
override func prepareForReuse() {
super.prepareForReuse()
twoLineHelper.setupDynamicFonts()
}
func mergeAccessibilityLabels(_ views: [AnyObject?]? = nil) {
twoLineHelper.mergeAccessibilityLabels(views)
}
}
private class TwoLineCellHelper {
weak var container: UIView?
var textLabel: UILabel!
var detailTextLabel: UILabel!
var imageView: UIImageView!
var hasRightBadge: Bool = false
// TODO: Not ideal. We should figure out a better way to get this initialized.
func setUpViews(_ container: UIView, textLabel: UILabel, detailTextLabel: UILabel, imageView: UIImageView) {
self.container = container
self.textLabel = textLabel
self.detailTextLabel = detailTextLabel
self.imageView = imageView
if let headerView = self.container as? UITableViewHeaderFooterView {
headerView.contentView.backgroundColor = UIColor.clear
} else {
self.container?.backgroundColor = UIColor.clear
}
textLabel.textColor = TwoLineCellUX.TextColor
detailTextLabel.textColor = TwoLineCellUX.DetailTextColor
setupDynamicFonts()
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = 6 //hmm
imageView.layer.masksToBounds = true
}
func setupDynamicFonts() {
textLabel.font = DynamicFontHelper.defaultHelper.DeviceFontHistoryPanel
detailTextLabel.font = DynamicFontHelper.defaultHelper.SmallSizeRegularWeightAS
}
func layoutSubviews() {
guard let container = self.container else {
return
}
let height = container.frame.height
let textLeft = TwoLineCellUX.ImageSize + 2 * TwoLineCellUX.BorderViewMargin
let textLabelHeight = textLabel.intrinsicContentSize.height
let detailTextLabelHeight = detailTextLabel.intrinsicContentSize.height
var contentHeight = textLabelHeight
if detailTextLabelHeight > 0 {
contentHeight += detailTextLabelHeight + TwoLineCellUX.DetailTextTopMargin
}
let textRightInset: CGFloat = hasRightBadge ? (TwoLineCellUX.BadgeSize + TwoLineCellUX.BadgeMargin) : 0
imageView.frame = CGRect(x: TwoLineCellUX.BorderViewMargin, y: (height - TwoLineCellUX.ImageSize) / 2, width: TwoLineCellUX.ImageSize, height: TwoLineCellUX.ImageSize)
textLabel.frame = CGRect(x: textLeft, y: (height - contentHeight) / 2,
width: container.frame.width - textLeft - TwoLineCellUX.BorderViewMargin - textRightInset, height: textLabelHeight)
detailTextLabel.frame = CGRect(x: textLeft, y: textLabel.frame.maxY + TwoLineCellUX.DetailTextTopMargin,
width: container.frame.width - textLeft - TwoLineCellUX.BorderViewMargin - textRightInset, height: detailTextLabelHeight)
}
func setLines(_ text: String?, detailText: String?) {
if text?.isEmpty ?? true {
textLabel.text = detailText
detailTextLabel.text = nil
} else {
textLabel.text = text
detailTextLabel.text = detailText
}
}
func mergeAccessibilityLabels(_ labels: [AnyObject?]?) {
let labels = labels ?? [textLabel, imageView, detailTextLabel]
let label = labels.map({ (label: AnyObject?) -> NSAttributedString? in
var label = label
if let view = label as? UIView {
label = view.value(forKey: "accessibilityLabel") as (AnyObject?)
}
if let attrString = label as? NSAttributedString {
return attrString
} else if let string = label as? String {
return NSAttributedString(string: string)
} else {
return nil
}
}).filter({
$0 != nil
}).reduce(NSMutableAttributedString(string: ""), {
if $0.length > 0 {
$0.append(NSAttributedString(string: ", "))
}
$0.append($1!)
return $0
})
container?.isAccessibilityElement = true
container?.setValue(NSAttributedString(attributedString: label), forKey: "accessibilityLabel")
}
}