英文:
String text alignment by decimal point (Swift)
问题
有没有一种方法可以在多个字符串中实现这种数字对齐,最好是在界面构建器中?请查看附带的屏幕截图。
英文:
Is there a way to achieve such kind of alignment of numbers in multiple strings, preferably in interface builder? Please see the attached screenshot.
答案1
得分: 0
以下是翻译好的代码部分:
class OffsetNumberViewController: UIViewController {
var values: [NSDecimalNumber] = [] {
didSet {
tableView.reloadData()
}
}
private lazy var tableView: UITableView = {
let view = UITableView()
view.delegate = self
view.dataSource = self
return view
}()
private lazy var numberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.decimalSeparator = "."
formatter.usesGroupingSeparator = true
formatter.groupingSeparator = ","
formatter.groupingSize = 3
formatter.maximumFractionDigits = 5
return formatter
}()
private lazy var referenceView = {
let view = UIView()
view.isHidden = true
self.view.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
self.view.addConstraints([
.init(item: view, attribute: .trailing, relatedBy: .lessThanOrEqual, toItem: self.view, attribute: .trailing, multiplier: 1.0, constant: 0.0),
.init(item: view, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1.0, constant: 0.0),
.init(item: view, attribute: .bottom, relatedBy: .equal, toItem: self.view, attribute: .bottom, multiplier: 1.0, constant: 0.0)
])
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(NumberTableViewCell.self, forCellReuseIdentifier: "amountCell")
tableView.frame = view.bounds
view.addSubview(tableView)
values = generateRandomValues(count: 1000)
}
private func generateRandomValues(count: Int) -> [NSDecimalNumber] {
(0..<count).map { index in
let startValue: Int = 1234567890
let maximumDivision = 5
let randomDivision: Int = 1<<Int.random(in: 0...maximumDivision)
return .init(integerLiteral: startValue).dividing(by: .init(integerLiteral: randomDivision))
}
}
private func formatValue(_ value: NSDecimalNumber) -> String {
numberFormatter.string(for: value) ?? "NaN"
}
}
// MARK: - UITableViewDelegate, UITableViewDataSource
extension OffsetNumberViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
values.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "amountCell", for: indexPath) as? NumberTableViewCell {
cell.setup(withNumberAsString: formatValue(values[indexPath.row]), decimalSeparator: numberFormatter.decimalSeparator)
return cell
} else {
return UITableViewCell()
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
(cell as? NumberTableViewCell)?.attachCenterTo(referenceView, parent: self.view)
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
(cell as? NumberTableViewCell)?.detachExternalConstraints()
}
}
// MARK: - NumberTableViewCell
extension OffsetNumberViewController {
class NumberTableViewCell: UITableViewCell {
lazy private var leftSideLabel: UILabel = UILabel()
lazy private var rightSideLabel: UILabel = UILabel()
lazy private var separatorLabel: UILabel = UILabel()
private var currentExternalConstraints: [NSLayoutConstraint] = []
lazy private var stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.alignment = .fill
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.addArrangedSubview(leftSideLabel)
stackView.addArrangedSubview(separatorLabel)
stackView.addArrangedSubview(rightSideLabel)
addSubview(stackView)
addConstraints([
.init(item: stackView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: -12.0),
.init(item: stackView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0.0),
.init(item: stackView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0),
])
return stackView
}()
func setup(withNumberAsString numberString: String, decimalSeparator: String) {
let components = numberString.components(separatedBy: decimalSeparator)
let _ = stackView
separatorLabel.text = decimalSeparator
if components.count == 1 {
leftSideLabel.text = components[0]
rightSideLabel.text = ""
separatorLabel.alpha = 0
} else if components.count == 2 {
leftSideLabel.text = components[0]
rightSideLabel.text = components[1]
separatorLabel.alpha = 1
} else {
// Something went wrong
leftSideLabel.text = ""
rightSideLabel.text = "error"
separatorLabel.alpha = 0
}
}
func detachExternalConstraints() {
currentExternalConstraints.forEach { constrain in
constrain.isActive = false
}
currentExternalConstraints = []
}
func attachCenterTo(_ referenceView: UIView, parent: UIView) {
currentExternalConstraints = [
.init(item: separatorLabel,
attribute: .centerX,
relatedBy: .greaterThanOrEqual,
toItem: referenceView,
attribute: .centerX,
multiplier: 1.0,
constant: 0.0),
.init(item: referenceView,
attribute: .centerX,
relatedBy: .greaterThanOrEqual,
toItem: separatorLabel,
attribute: .centerX,
multiplier: 1.0,
constant: 0.0)
]
parent.addConstraints(currentExternalConstraints)
}
}
}
英文:
One approach to achieve this is by using a table view and programmatically adding constraints to align the separator symbol. This method offers scalability as it only includes elements visible on the screen. An interesting aspect of this approach is that the separator's position may change based on the largest offset currently on the screen. Whether this behavior is desired or not depends on your specific requirements.
In this solution, I suggest splitting the string into three components and placing them into three separate labels: one for the content before the separator, one for the separator itself, and another for the content after the separator. Additionally, create an invisible reference view at the top level, which will be used to connect the separator label.
Although the following code is implemented programmatically for clarity in understanding constraint connections, it is recommended to move most of the code into the storyboard for better organization and maintainability.
I hope this code snippet helps you solve your problem.
class OffsetNumberViewController: UIViewController {
var values: [NSDecimalNumber] = [] {
didSet {
tableView.reloadData()
}
}
private lazy var tableView: UITableView = {
let view = UITableView()
view.delegate = self
view.dataSource = self
return view
}()
private lazy var numberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.decimalSeparator = "."
formatter.usesGroupingSeparator = true
formatter.groupingSeparator = ","
formatter.groupingSize = 3
formatter.maximumFractionDigits = 5
return formatter
}()
private lazy var referenceView = {
let view = UIView()
view.isHidden = true
self.view.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
self.view.addConstraints([
.init(item: view, attribute: .trailing, relatedBy: .lessThanOrEqual, toItem: self.view, attribute: .trailing, multiplier: 1.0, constant: 0.0),
.init(item: view, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1.0, constant: 0.0),
.init(item: view, attribute: .bottom, relatedBy: .equal, toItem: self.view, attribute: .bottom, multiplier: 1.0, constant: 0.0)
])
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(NumberTableViewCell.self, forCellReuseIdentifier: "amountCell")
tableView.frame = view.bounds
view.addSubview(tableView)
values = generateRandomValues(count: 1000)
}
private func generateRandomValues(count: Int) -> [NSDecimalNumber] {
(0..<count).map { index in
let startValue: Int = 1234567890
let maximumDivision = 5
let randomDivision: Int = 1<<Int.random(in: 0...maximumDivision)
return .init(integerLiteral: startValue).dividing(by: .init(integerLiteral: randomDivision))
}
}
private func formatValue(_ value: NSDecimalNumber) -> String {
numberFormatter.string(for: value) ?? "NaN"
}
}
// MARK: - UITableViewDelegate, UITableViewDataSource
extension OffsetNumberViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
values.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "amountCell", for: indexPath) as? NumberTableViewCell {
cell.setup(withNumberAsString: formatValue(values[indexPath.row]), decimalSeparator: numberFormatter.decimalSeparator)
return cell
} else {
return UITableViewCell()
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
(cell as? NumberTableViewCell)?.attachCenterTo(referenceView, parent: self.view)
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
(cell as? NumberTableViewCell)?.detachExternalConstraints()
}
}
// MARK: - NumberTableViewCell
extension OffsetNumberViewController {
class NumberTableViewCell: UITableViewCell {
lazy private var leftSideLabel: UILabel = UILabel()
lazy private var rightSideLabel: UILabel = UILabel()
lazy private var separatorLabel: UILabel = UILabel()
private var currentExternalConstraints: [NSLayoutConstraint] = []
lazy private var stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.alignment = .fill
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.addArrangedSubview(leftSideLabel)
stackView.addArrangedSubview(separatorLabel)
stackView.addArrangedSubview(rightSideLabel)
addSubview(stackView)
addConstraints([
.init(item: stackView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: -12.0),
.init(item: stackView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0.0),
.init(item: stackView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0),
])
return stackView
}()
func setup(withNumberAsString numberString: String, decimalSeparator: String) {
let components = numberString.components(separatedBy: decimalSeparator)
let _ = stackView
separatorLabel.text = decimalSeparator
if components.count == 1 {
leftSideLabel.text = components[0]
rightSideLabel.text = ""
separatorLabel.alpha = 0
} else if components.count == 2 {
leftSideLabel.text = components[0]
rightSideLabel.text = components[1]
separatorLabel.alpha = 1
} else {
// Something went wrong
leftSideLabel.text = ""
rightSideLabel.text = "error"
separatorLabel.alpha = 0
}
}
func detachExternalConstraints() {
currentExternalConstraints.forEach { constrain in
constrain.isActive = false
}
currentExternalConstraints = []
}
func attachCenterTo(_ referenceView: UIView, parent: UIView) {
currentExternalConstraints = [
.init(item: separatorLabel,
attribute: .centerX,
relatedBy: .greaterThanOrEqual,
toItem: referenceView,
attribute: .centerX,
multiplier: 1.0,
constant: 0.0),
.init(item: referenceView,
attribute: .centerX,
relatedBy: .greaterThanOrEqual,
toItem: separatorLabel,
attribute: .centerX,
multiplier: 1.0,
constant: 0.0)
]
parent.addConstraints(currentExternalConstraints)
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论