이번 현장실습을 진행하면서, 아래와 같은 기능을 구현 했어야했다.
필터링을 진행하는 버튼을 생성해야 했다.
- 생성되는 버튼이 화면 디바이스의 width를 넘기지 않으면 1줄만으로 필터 버튼 영역을 구성,
- 만약 디바이스 width를 넘긴다면 2줄로 만들어줘야 하는 필터 버튼 영역을 만들어야 했다.
내가 설정한 로직은 아래와 같다.
UIStackView
로 구성을 한다.currentRow
- 가로 한줄을 추가해줄 때 사용하기 위한
UIStackView
- 가로 한줄을 추가해줄 때 사용하기 위한
mainStackView
- 전체 필터 버튼의 영역을 관리하는
UIStackView
- 전체 필터 버튼의 영역을 관리하는
코드 구성은 아래와 같다.
- 특이사항
- refreshControl은 테스트 편하게 해보려고 넣었다.
- UIApplication Extension도 기존 레퍼런스를 참고했다.
- Button 설정은 configuration을 사용해봤다.
//
// ViewController.swift
// DynamicStackView
//
// Created by 박성근 on 12/29/24.
//
import UIKit
class ViewController: UIViewController {
// MARK: - UIComponents
private let mainStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .leading
stackView.spacing = 10
return stackView
}()
private lazy var refreshControl: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
return refreshControl
}()
// MARK: - Filter Contents
private let filterContents: [[String: Int]] = [["음식": 80], ["실내": 20], ["실외": 17], ["메뉴 * 정보": 18], ["주차": 3], ["전체": 10], ["이걸 왜 넣어": 999]]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setLayout()
createFilterContentView()
}
// FilterContentView 생성
private func createFilterContentView() {
let filterContents = requestFilterContents()
let maxWidth = UIApplication.screenWidth - 32 // margin
var currentRow: UIStackView = createHorizontalStack()
var currentWidth: CGFloat = 0
for content in filterContents {
let button = createRectangleButton(params: content)
// 버튼 크기 계산
let buttonSize = button.sizeThatFits(CGSize(width: maxWidth, height: 30))
let buttonWidth = buttonSize.width
// 이 버튼을 추가했을 때 최대 너비를 초과하는지 확인
if currentWidth + buttonWidth > maxWidth {
// 현재 행을 mainStackView에 추가하고 새로운 행 생성
mainStackView.addArrangedSubview(currentRow)
currentRow = createHorizontalStack()
currentWidth = 0
}
// 현재 행에 버튼 추가
currentRow.addArrangedSubview(button)
currentWidth += buttonWidth + currentRow.spacing
}
// 마지막 행에 버튼이 있다면 추가
if currentRow.arrangedSubviews.count > 0 {
mainStackView.addArrangedSubview(currentRow)
}
}
private func requestFilterContents() -> [[String: Int]] {
let randomCount = Int.random(in: 3...filterContents.count)
let randomResults = filterContents.shuffled().prefix(randomCount)
return Array(randomResults)
}
}
// MARK: - Setup
extension ViewController {
private func setLayout() {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.refreshControl = refreshControl
mainStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
scrollView.addSubview(mainStackView)
NSLayoutConstraint.activate([
// ScrollView Constraints
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
// MainStackView Constraints
mainStackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 16),
mainStackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 16),
mainStackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -16),
mainStackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor)
])
}
}
// MARK: - UI Create Logic / Action
extension ViewController {
private func createHorizontalStack() -> UIStackView {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 10
stackView.alignment = .leading
stackView.distribution = .fillProportionally
return stackView
}
private func createRectangleButton(params: [String: Int]) -> UIButton {
var configuration = UIButton.Configuration.filled()
configuration.cornerStyle = .capsule
configuration.baseBackgroundColor = .systemBlue
configuration.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 20, bottom: 12, trailing: 20)
if let key = params.keys.first, let value = params.values.first {
var container = AttributeContainer()
container.font = UIFont.systemFont(ofSize: 16, weight: .regular)
configuration.attributedTitle = AttributedString("\(key)(\(value))", attributes: container)
}
let button = UIButton(configuration: configuration)
button.configuration?.buttonSize = .large
return button
}
@objc private func handleRefresh() {
mainStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
createFilterContentView()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.refreshControl.endRefreshing()
}
}
}
extension UIApplication {
static var screenSize: CGSize {
guard let windowScene = shared.connectedScenes.first as? UIWindowScene else {
return UIScreen.main.bounds.size
}
return windowScene.screen.bounds.size
}
static let screenHeight: CGFloat = screenSize.height
static let screenWidth: CGFloat = screenSize.width
static let isMinimumSizeDevice: Bool = screenSize.height <= 667
}
실행 영상
화면 기록 2024-12-29 오후 9.43.35.mov
3.60MB
후기
- shuffled( )를 해주면서 하는데, 확실히 UI를 위해선
- API로부터 받는 값이 정렬된 형태이든지, 프론트에서 정렬을 해줘야 하는 작업이 필요할 것 같다.
- 가장 먼저 생각나서, 작업을 진행했던 내용이 UIStackView여서 그렇지, 다른 대안들도 있을 수 있다.
- 지금은 필터 버튼의 갯수가 적어서 크게 성능 저하가 되지는 않지만, 만약 100 ~ 200개의 버튼이 있다면?
- 재사용을 하는 메커니즘이 없어 메모리 사용량이 증가할 수 있겠다.
- 또한 스크롤 / 애니메이션 / 유저 인터렉션 등을 구현할 때 요구하는 사항들이 많아진다.
- CollectionView를 이용하는 것도 대안이 될 수 있다.
- 위의 단점들을 극복할 수 있지만 / 이런 간단한 화면에서는 오버엔지니어링일 수도 있다.
만약 CollectionView로 해야만 한다면 아래와 같은 상황이라면 적용을 할 것 같다. (개인적인 견해다.)
- 필터 항목이 많을 때
- 드래그로 순서 변경 등 추가 기능이 필요할 때
- 애니메이션이나 유저 인터랙션이 필요할 때
반응형
'iOS' 카테고리의 다른 글
MVVM을 직접 사용해보며 (0) | 2024.11.10 |
---|---|
단방향 / 양방향 데이터 흐름 (0) | 2024.11.10 |
MVVM, Binding (0) | 2024.10.30 |
Clean Architecture 적용해보기 (2) | 2024.09.29 |
추상화, 그리고 타입 캐스팅? (0) | 2024.09.10 |