3중으로 중첩된 UIStackView를 구현할 일이 있어서 해당 내용을 기록헀습니다
Vertical StackView라면 내부 구성요소의 모든 높이,
Horizontal StackView라면 내부 구성요소의 모든 너비가 명확하게 계산될 수 있도록 하면 됩니다.
결과를 보여주는 텍스트 뷰를 가변적으로 조정되게 하여, 여러 화면 사이즈에 대응할 수 있도록 합니다 (ppt로 만들었습니다)
UI를 아래처럼 구성했습니다
3-depth의 두 Vertical StackView는 UI 상으로 동일해서 클래스로 따로 분리해내면 재사용이 용이할 것으로 보입니다
가장 바깥의 Vertical StackView 내의 모든 컴포넌트의 높이가 명확하도록 설정합니다
단, UITextView는 높이가 가변적이어야 하므로 최소 높이를 지정합니다
Horizontal StackView의 높이는 내부 구성요소를 채워야 정확하게 정해지기 때문에 지금은 임시값으로 넣어줍니다
Horizontal StackView 내부의 두 UIView는 distribution = .fillEqualy로 인해 너비가 명확하게 정해질 수 있기 때문에 따로 너비를 설정하지 않아도 괜찮습니다
위치가 제대로 잡혔는지 확인하기 위해 임시 배경색을 지정합니다
final class ViewController: UIViewController {
private lazy var hStackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [
{
let view = UIView()
view.backgroundColor = .green
return view
}(),
{
let view = UIView()
view.backgroundColor = .green
return view
}(),
])
view.axis = .horizontal
view.spacing = 20
view.distribution = .fillEqually
view.backgroundColor = .systemGray
return view
}()
private var separatorView: UIView = {
let view = UIView()
view.backgroundColor = .systemGray
return view
}()
private var resultTitleLabel: UILabel = {
let label = UILabel()
label.backgroundColor = .red
return label
}()
private var resultTextView: UITextView = {
let view = UITextView()
view.backgroundColor = .brown
return view
}()
private lazy var vStackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [
hStackView,
separatorView,
resultTitleLabel,
resultTextView
])
view.axis = .vertical
view.spacing = 20
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
layout()
}
private func layout() {
view.backgroundColor = .white
[vStackView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
let horizontalMargin = 15.0
let verticalMargin = 10.0
NSLayoutConstraint.activate([
vStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: verticalMargin),
vStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: horizontalMargin),
vStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -1 * horizontalMargin),
vStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -1 * verticalMargin),
hStackView.heightAnchor.constraint(equalToConstant: 300),
separatorView.heightAnchor.constraint(equalToConstant: 2),
resultTitleLabel.heightAnchor.constraint(equalToConstant: 30),
resultTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 1)
])
}
}
별도의 클래스로 제작합니다
UI구성은 아래와 같습니다 (위 이미지 참고)
Vertical StackView의 Rect를 SuperView와 동일하게 설정합니다
UIImageView는 가로, 세로를 1:1의 비율로 설정하고, 가로의 길이는 SuperView와 동일하게 설정합니다
Vertical StackView의 alignment가 .fill이 기본이기 때문에 너비는 SuperView만큼 늘어납니다
SuperView의 너비가 명확하게 계산되어야 ImageView의 너비 & 높이에 문제가 생기지 않고 Vertical StackView의 높이도 정해질 수 있습니다
다행히도 Horizontal StackView의 distribution = .fillEqualy로 인해 명확하게 계산되기 때문에 괜찮습니다
Horizontal StackView의 너비 정해짐
=> SectionView 너비 정해짐
=> Vertical StackView의 너비 정해짐
=> ImageView 너비 정해짐
=> ImageView 높이 정해짐
=> Vertical StackView 높이 정해짐
=> SectionView 높이 정해짐
나머지 컴포넌트의 높이를 상수로 설정해줍니다
final class CompanyView: UIView {
private lazy var companyNameLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.textAlignment = .center
label.font = .systemFont(ofSize: 24, weight: .bold)
return label
}()
private lazy var imageView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .systemGray6
view.contentMode = .scaleAspectFit
return view
}()
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.textAlignment = .center
label.font = .systemFont(ofSize: 20)
return label
}()
private var actionButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("실행", for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 24)
return button
}()
private lazy var vStackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [
companyNameLabel,
imageView,
descriptionLabel,
actionButton
])
view.axis = .vertical
view.spacing = 10
return view
}()
init(companyName: String, solutionDescription: String, action: UIAction) {
super.init(frame: .zero) // auto layout 사용
companyNameLabel.text = companyName
descriptionLabel.text = solutionDescription
actionButton.addAction(action, for: .touchUpInside)
layout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func layout() {
[vStackView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
NSLayoutConstraint.activate([
vStackView.topAnchor.constraint(equalTo: topAnchor),
vStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
vStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
vStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
companyNameLabel.heightAnchor.constraint(equalToConstant: 30),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
descriptionLabel.heightAnchor.constraint(equalToConstant: 26),
actionButton.heightAnchor.constraint(equalToConstant: 30),
])
}
}
SectionView의 높이가 명확하게 계산될 수 있기 때문에 Horizontal StackView의 높이를 SectionView와 같게 합니다
기존
hStackView.heightAnchor.constraint(equalToConstant: 300),
변경
hStackView.heightAnchor.constraint(equalTo: companyAView.heightAnchor),
이쪽은 전체 소스코드로 확인해주세요
특별한 건 없습니다
final class CompanyView: UIView {
private lazy var companyNameLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.textAlignment = .center
label.font = .systemFont(ofSize: 24, weight: .bold)
return label
}()
private lazy var imageView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .systemGray6
view.contentMode = .scaleAspectFit
return view
}()
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.textAlignment = .center
label.font = .systemFont(ofSize: 20)
return label
}()
private var actionButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("실행", for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 24)
return button
}()
private lazy var vStackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [
companyNameLabel,
imageView,
descriptionLabel,
actionButton
])
view.axis = .vertical
view.spacing = 10
return view
}()
init(companyName: String, solutionDescription: String, action: UIAction) {
super.init(frame: .zero) // auto layout 사용
companyNameLabel.text = companyName
descriptionLabel.text = solutionDescription
actionButton.addAction(action, for: .touchUpInside)
layout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func layout() {
[vStackView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
NSLayoutConstraint.activate([
vStackView.topAnchor.constraint(equalTo: topAnchor),
vStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
vStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
vStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
companyNameLabel.heightAnchor.constraint(equalToConstant: 30),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
descriptionLabel.heightAnchor.constraint(equalToConstant: 26),
actionButton.heightAnchor.constraint(equalToConstant: 30),
])
}
}
final class ViewController: UIViewController {
private lazy var companyAView = CompanyView(
companyName: "회사A",
solutionDescription: "솔루션A",
action: UIAction(handler: { _ in
print("회사A")
}))
private var companyBView = CompanyView(
companyName: "회사B",
solutionDescription: "솔루션B",
action: UIAction(handler: { _ in
print("회사B")
}))
private lazy var hStackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [
companyAView,
companyBView
])
view.axis = .horizontal
view.distribution = .fillEqually
view.spacing = 20
return view
}()
private var separatorView: UIView = {
let view = UIView()
view.backgroundColor = .systemGray
return view
}()
private var resultTitleLabel: UILabel = {
let label = UILabel()
label.text = "결과"
label.textAlignment = .center
label.font = .systemFont(ofSize: 24, weight: .bold)
return label
}()
private var resultTextView: UITextView = {
let view = UITextView()
view.backgroundColor = .systemGray6
view.isEditable = false
view.font = .systemFont(ofSize: 18)
view.text = "" // 생략
return view
}()
private lazy var vStackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [
hStackView,
separatorView,
resultTitleLabel,
resultTextView
])
view.axis = .vertical
view.spacing = 20
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
layout()
}
private func layout() {
view.backgroundColor = .white
[vStackView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
let horizontalMargin = 15.0
let verticalMargin = 10.0
NSLayoutConstraint.activate([
vStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: verticalMargin),
vStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: horizontalMargin),
vStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -1 * horizontalMargin),
vStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -1 * verticalMargin),
hStackView.heightAnchor.constraint(equalTo: companyAView.heightAnchor),
separatorView.heightAnchor.constraint(equalToConstant: 2),
resultTitleLabel.heightAnchor.constraint(equalToConstant: 30),
resultTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 1)
])
}
}
UIkit으로 뷰를그릴때 스택뷰를 거의안쓰다싶이했는데 정말잘쓰면 편하다고하더라고요 ㅎㅎ
swiftUI를 해보니까 전체적인 메커니즘이 스택뷰기반이라는 느낌도강했던거같아요
이게 애플에서 앞으로 밀고가려는 레이아웃방식이라는 느낌이들어서 유킷에서도 스택뷰를활용을 잘해봐야겠다는생각이드네요
혹시 flexlayout에관해서는 어떻게생각하시는지 궁금하네요!