You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SessionUIKit/Components/ScrollableLabelView.swift

156 lines
4.6 KiB
Swift

// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
import UIKit
class ScrollableLabelView: UIView {
private var oldSize: CGSize = .zero
private var layoutLoopCounter: Int = 0
var canScroll: Bool = false {
didSet {
guard canScroll != oldValue else { return }
updateContentSizeIfNeeded()
}
}
var font: UIFont {
get { label.font }
set { label.font = newValue }
}
var text: String? {
get { label.text }
set {
guard label.text != newValue else { return }
label.text = newValue
updateContentSizeIfNeeded()
}
}
var attributedText: NSAttributedString? {
get { label.attributedText }
set {
guard label.attributedText != newValue else { return }
label.attributedText = newValue
updateContentSizeIfNeeded()
}
}
var themeTextColor: ThemeValue? {
get { label.themeTextColor }
set { label.themeTextColor = newValue }
}
var textAlignment: NSTextAlignment {
get { label.textAlignment }
set { label.textAlignment = newValue }
}
var lineBreakMode: NSLineBreakMode {
get { label.lineBreakMode }
set { label.lineBreakMode = newValue }
}
var numberOfLines: Int {
get { label.numberOfLines }
set { label.numberOfLines = newValue }
}
var maxNumberOfLinesWhenScrolling: Int = 5 {
didSet {
guard maxNumberOfLinesWhenScrolling != oldValue else { return }
updateContentSizeIfNeeded()
}
}
// MARK: - Initialization
init() {
super.init(frame: .zero)
setupViews()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - UI Components
private lazy var labelHeightAnchor: NSLayoutConstraint = label.set(.height, to: .height, of: scrollView).setting(isActive: false)
private lazy var scrollViewHeightAnchor: NSLayoutConstraint = scrollView.set(.height, to: 0).setting(isActive: false)
private let scrollView: UIScrollView = UIScrollView()
private let label: UILabel = UILabel()
// MARK: - Layout
private func setupViews() {
addSubview(scrollView)
scrollView.addSubview(label)
}
private func setupConstraints() {
scrollView.pin(to: self)
label.setContentHugging(.vertical, to: .required)
label.pin(to: scrollView)
label.set(.width, to: .width, of: scrollView)
}
override func layoutSubviews() {
super.layoutSubviews()
guard frame.size != oldSize else {
layoutLoopCounter = 0
return
}
updateContentSizeIfNeeded()
}
private func updateContentSizeIfNeeded() {
// Ensure we don't get stuck in an infinite layout loop somehow
guard layoutLoopCounter < 5 else { return }
// Update the contentSize of the scrollView to match the size of the label
scrollView.contentSize = label.sizeThatFits(
CGSize(width: scrollView.bounds.width, height: CGFloat.greatestFiniteMagnitude)
)
// If scrolling is enabled and the maximum height we want to show is smaller than the scrollable height
// then we need to fix the height of the scroll view to our desired maximum, other
let maxCalculatedHeight: CGFloat = (label.font.lineHeight * CGFloat(maxNumberOfLinesWhenScrolling))
switch (canScroll, maxCalculatedHeight <= scrollView.contentSize.height) {
case (false, _), (true, false):
scrollViewHeightAnchor.isActive = false
labelHeightAnchor.isActive = true
case (true, true):
labelHeightAnchor.isActive = false
scrollViewHeightAnchor.constant = maxCalculatedHeight
scrollViewHeightAnchor.isActive = true
}
oldSize = frame.size
// The view should have the same height as the scrollView, if it doesn't then we might need to relayout
// again to ensure the frame size is correct
guard
scrollView.frame.size.height < CGFloat.leastNonzeroMagnitude ||
abs(frame.size.height - scrollView.frame.size.height) > CGFloat.leastNonzeroMagnitude
else { return }
layoutLoopCounter += 1
setNeedsLayout()
layoutIfNeeded()
}
}