Search code examples
swiftuikituistackview

Custom stack view alignment


enter image description here

enter image description here

import UIKit
import TinyConstraints

final class CountDownStackView: UIStackView {
    func configure(withData data: [[String]]) {
           data.forEach { rowData in
               let verticalStackView = UIStackView()
               verticalStackView.axis = .vertical
               verticalStackView.spacing = 8
               
               rowData.forEach { text in
                   let label = UILabel()
                   label.text = text
                   label.backgroundColor = .lightGray
                   label.textAlignment = .center
                   verticalStackView.addArrangedSubview(label)
               }
               
               let horizontalStackView = UIStackView()
               horizontalStackView.axis = .horizontal
               horizontalStackView.spacing = 5
               
               horizontalStackView.addArrangedSubview(verticalStackView)
               
               let parser = UILabel()
               parser.text = ":"
               parser.customizeLabel(font: UIFont.systemFont(ofSize: 18),textColor: .white)
               horizontalStackView.addArrangedSubview(parser)
               
               addArrangedSubview(horizontalStackView)
           }
       }
    }

I'm using this code blog and I can't find where I went wrong and where I need to fix it.

the first image is the result of my codes, but I am trying to capture the situation in the second image

  let data: [[String]] = [
        ["02", " Hour"],
        ["43","minute"],
        ["23","second"]]

sample data


Solution

  • You only need one horizontal stack view but your code will create 3. Since self is already a stack view, self should be the one and only horizontal stack view. Here's a rework of your class:

    final class CountDownStackView: UIStackView {
        func configure(withData data: [[String]]) {
            axis = .horizontal
            distribution = .equalSpacing
            alignment = .center
            spacing = 5
    
            for (column, rowData) in data.enumerated() {
                var labels = [UILabel]()
                for (row, text) in rowData.enumerated() {
                    let label = UILabel(frame: .zero)
                    label.font = row == 0 ? .preferredFont(forTextStyle: .title1) : .preferredFont(forTextStyle: .body)
                    label.textColor = column + 1 == data.count ? .systemGray : .label
                    label.text = text
                    label.textAlignment = .left
                    labels.append(label)
                }
                let verticalStackView = UIStackView(arrangedSubviews: labels)
                verticalStackView.axis = .vertical
                verticalStackView.distribution = .equalSpacing
                verticalStackView.alignment = .leading
                verticalStackView.spacing = 4
    
                addArrangedSubview(verticalStackView)
    
                if column + 1 < data.count {
                    let parser = UILabel()
                    parser.text = ":"
                    parser.customizeLabel(font: UIFont.systemFont(ofSize: 18), textColor: .label)
                    parser.sizeToFit()
                    addArrangedSubview(parser)
                }
            }
        }
    }
    

    Then call it with something like:

    let data: [[String]] = [
        ["02", "Hour"],
        ["43", "Minute"],
        ["23", "Second"],
    ]
    
    let stack = CountDownStackView()
    stack.configure(withData: data)
    

    This gives you a layout that is much closer to the 2nd picture. But the two colons don't look right. I would add the colons as strings in your data array. Update the class as follows:

    final class CountDownStackView: UIStackView {
        func configure(withData data: [[String]]) {
            axis = .horizontal
            distribution = .equalSpacing
            alignment = .center
            spacing = 5
    
            for (column, rowData) in data.enumerated() {
                var labels = [UILabel]()
                for (row, text) in rowData.enumerated() {
                    let label = UILabel(frame: .zero)
                    label.font = row == 0 ? .preferredFont(forTextStyle: .title1) : .preferredFont(forTextStyle: .body)
                    label.textColor = column + 1 == data.count ? .systemGray : .label
                    label.text = text
                    label.textAlignment = .left
                    labels.append(label)
                }
                let verticalStackView = UIStackView(arrangedSubviews: labels)
                verticalStackView.axis = .vertical
                verticalStackView.distribution = .equalSpacing
                verticalStackView.alignment = .leading
                verticalStackView.spacing = 4
    
                addArrangedSubview(verticalStackView)
            }
        }
    }
    

    Then create it as follows:

    let data: [[String]] = [
        ["02", "Hour"],
        [":", " "],
        ["43", "Minute"],
        [":", " "],
        ["23", "Second"],
    ]
    
    let stack = CountDownStackView()
    stack.configure(withData: data)
    

    This gives a better result with the layout.

    While this answer gives you a solution to the layout issues with your custom stack view subclass, I would suggest that this whole approach is far from ideal for implementing some sort of countdown view. Your current CountDownStackView makes it very difficult to update any of the labels as the timer counts down.

    While I'll leave the details for another question (since this is beyond the scope of your original question), you really should create a subclass of UIView that makes use of a stack view of labels. It should also use a Timer that can be used to update the labels in the stack view.