🗜️

段階を示すUIの画面をAutoLayoutで

2024/02/22に公開

以前こんなツイートをしました。

https://x.com/samekard_dev/status/1659759564743655426?s=20

将棋など甲乙に分かれて行うゲームの段階を示す画面では、こういうデザインにして出来るだけ縮めてスペースを有効に使いたいですね。これを(SwiftUIではなく)AutoLayoutでやります。

完成品

現在の状態がどれなのか示すUIはこの記事から省きます。

登場人物

各工程毎に

  • 点(赤を塗る、UIViewのサブクラス)
  • 文章(UILabel)

必要な制約

  • ペアになっている点と文はCenterYが揃っている

隣あう上下の工程(AとBとする)は

  • Aの点の下端よりもBの文章の上端は下
  • Aの文章の下端よりもBの点の上端は下

です。
最後に

  • できるだけ縦方向を詰める

があります。

コード

pp 点(UIViewのサブクラス)の配列
dd 文章(UILabel)の配列

ペアになっている点と文はCenterYを揃えるところが

for i in 0..<4 {
    pp[i].centerYAnchor.constraint(equalTo: dd[i].centerYAnchor)
        .isActive = true
}

です。
上下関係のこれ以上踏み込んではいけない制約が

for i in 0..<3 {
    pp[i].bottomAnchor.constraint(lessThanOrEqualTo: dd[i + 1].topAnchor)
        .isActive = true
    dd[i].bottomAnchor.constraint(lessThanOrEqualTo: pp[i + 1].topAnchor)
        .isActive = true
}

です。

最後に、手元の確認ではあってもなくても変わりませんでしたが、理論上は広がってしまうことがありえる状態なので、念の為に縮める制約も付けます。

let shrink = dd[3].bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
shrink.priority = UILayoutPriority(700.0)
shrink.isActive = true

ちなみに700は、文章が潰されるのに抵抗する力が750なのでそれに負ける値というのが根拠です。
ここに760を設定すると、文章が潰されて

となります。点のサイズは1000の力で設定してあるので生き残ります。

ソース全体

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        var pp: [PointView] = []
        var dd: [UILabel] = []
        for _ in 0..<4 {
            pp.append(PointView(frame: .zero))
            dd.append({
                let l = UILabel()
                l.numberOfLines = 0
                return l
            }())
        }
        dd[0].text = "春はあけぼの。やうやう白くなりゆく山ぎは、すこしあかりて、紫だちたる 雲のほそくたなびきたる。"
        dd[1].text = "夏は夜。月のころはさらなり。やみもなほ、蛍の多く飛びちがひたる。また、 ただ一つ二つなど、ほのかにうち光りて行くもをかし。雨など降るもをかし。"
        dd[2].text = "秋は夕暮れ。夕日のさして山の端いと近うなりたるに、烏の寝どころへ行く とて、三つ四つ、二つ三つなど、飛びいそぐさへあはれなり。まいて雁などの つらねたるが、いと小さく見ゆるはいとをかし。日入りはてて、風の音、虫の 音など、はたいふべきにあらず。"
        dd[3].text = "冬はつとめて。雪の降りたるはいふべきにもあらず、霜のいと白きも、また さらでもいと寒きに、火など急ぎおこして、炭もて渡るもいとつきづきし。 昼になりて、ぬるくゆるびもていけば、火桶の火も白き灰がちになりてわろし"
        
        for p in pp {
            view.addSubview(p)
            p.translatesAutoresizingMaskIntoConstraints = false
        }
        for d in dd {
            view.addSubview(d)
            d.translatesAutoresizingMaskIntoConstraints = false
        }
        for i in 0..<4 {
            pp[i].centerYAnchor.constraint(equalTo: dd[i].centerYAnchor).isActive = true
        }
        for i in 0..<4 {
            pp[i].widthAnchor.constraint(equalToConstant: 24).isActive = true
            pp[i].heightAnchor.constraint(equalToConstant: 24).isActive = true
        }
        for i in 0..<3 {
            pp[i].centerXAnchor.constraint(equalTo: pp[i + 1].centerXAnchor).isActive = true
        }
        for i in 0..<3 {
            dd[i].widthAnchor.constraint(equalTo: dd[i + 1].widthAnchor).isActive = true
        }
        for i in 0..<2 {
            dd[i * 2].leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor).isActive = true
            dd[i * 2].trailingAnchor.constraint(equalTo: pp[i * 2].leadingAnchor).isActive = true
            dd[i * 2 + 1].leadingAnchor.constraint(equalTo: pp[i * 2 + 1].trailingAnchor).isActive = true
            dd[i * 2 + 1].trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor).isActive = true
        }
        for i in 0..<3 {
            pp[i].bottomAnchor.constraint(lessThanOrEqualTo: dd[i + 1].topAnchor).isActive = true
            dd[i].bottomAnchor.constraint(lessThanOrEqualTo: pp[i + 1].topAnchor).isActive = true
        }

        dd[0].topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
        
        let shrink = dd[3].bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
        shrink.priority = UILayoutPriority(700.0)
        shrink.isActive = true
    }
}

class PointView: UIView {
    
    let padding: CGFloat = 6.0
    let radius: CGFloat = 4.0
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        let inner = UIView()
        inner.layer.backgroundColor = UIColor.red.cgColor
        inner.layer.cornerRadius = radius
        addSubview(inner)
        
        inner.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            inner.topAnchor.constraint(equalTo: topAnchor, constant: padding),
            inner.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -padding),
            inner.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding),
            inner.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding)
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Discussion