1. 구현한 것
어느 팀원을 선택하면 해당 팀원의 상세 페이지를 보여주는 화면을 제작하도록 했다.
해당 뷰에서 필요한 기술이 뭐가 있을지 만들기 전 생각해봤는데 1. 이미지 슬라이더 2. 그에 맞는 인디케이터 3. 칩 모양은 컬렉션뷰로 4. 화면 크기 생각해서 일부만 스크롤뷰로 구현
이정도를 해야했고, 이 포스트에선 가장 애먹었던 부분인 커스텀 슬라이더와 인디케이터에 대해 다루겠다.
이미지 슬라이더 자체는 컨셉이 컬렉션뷰고 화면 꽉채우고 페이지 가능하게 해야 하는 부분이라 썩 어렵지만은 않아서 일단 패스하고, 저 상단에 인디케이터는 보통 라이브러리나 기본 제공되는 형태가 점(dot)으로 되어있기에 커스텀할 필요가 있었다.
2. 구현 내용
우선 이런 커스텀 뷰 자체를 직접 만드는 능력이 아직 부족해 클로드에게 세세히 요청사항을 주고 구현 후 뜯어보며 어떤 코드이고 어떤 원리인지 분석하기로 했다.
우선 전체 코드는 다음과 같다.
class CustomPageControl: UIView {
private let stackView = UIStackView()
private var indicators = [UIView]()
private let indicatorWidth: CGFloat = 20
private let indicatorHeight: CGFloat = 3
private let spacing: CGFloat = 2
private let selectedColor = UIColor.white
private let defaultColor = UIColor.white.withAlphaComponent(0.3)
var currentPage: Int = 0 {
didSet {
updateIndicator()
}
}
var numberOfPages: Int = 3 {
didSet {
setupIndicators()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupStackView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented.")
}
private func setupStackView() {
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = spacing
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
private func setupIndicators() {
indicators.forEach { $0.removeFromSuperview() }
indicators.removeAll()
for _ in 0..<numberOfPages {
let indicator = UIView()
indicator.layer.cornerRadius = 2
indicator.backgroundColor = defaultColor
indicator.translatesAutoresizingMaskIntoConstraints = false
indicator.widthAnchor.constraint(equalToConstant: indicatorWidth).isActive = true
indicator.heightAnchor.constraint(equalToConstant: indicatorHeight).isActive = true
stackView.addArrangedSubview(indicator)
indicators.append(indicator)
}
updateIndicator()
}
private func updateIndicator() {
for (index, indicator) in indicators.enumerated() {
if index == currentPage {
UIView.animate(withDuration: 0.3) {
indicator.backgroundColor = self.selectedColor
}
}
else {
UIView.animate(withDuration: 0.3) {
indicator.backgroundColor = self.defaultColor
}
}
}
}
}
코드 중반부터 뜯어서 스토리를 연결하자면,
setupStackView는 초기 설정을 해주는 부분이다. 초기화 시 실행되는 코드고 인디케이터를 여러개 연속으로 띄우는 걸 스택뷰로 구현했다보니 스택뷰 자체의 제약조건과 프로퍼티를 잡아주도록 했다.
private func setupStackView() {
stackView.axis = .horizontal // 축을 수평으로 한다. 좌 우 스크롤 방식을 채택한다.
stackView.alignment = .center // 뷰를 기준으로 중심에 레이아웃을 그린다. 가운데 정렬.
stackView.spacing = spacing // 앞서 정의해둔 간격을 대입해 해당 수치만큼 띄울 수 있도록 한다.
addSubview(stackView) // 최상위 뷰에 스택뷰를 서브뷰로 추가한다.
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
]) // 스택뷰에 대한 제약조건을 설정하기전 세팅을 해두고 최상위 뷰의 센터에 스택뷰의 센터를 맞춘다.
}
setupIndicators는 인디케이터의 UI 관련된 부분을 설정하고 스택뷰에 해당 요소를 넣어 최종적으로 하나의 인디케이터를 생성하는것이다.
private func setupIndicators() {
indicators.forEach { $0.removeFromSuperview() } // 기존 인디케이터가 존재한다면 다시 세팅하기 위해 지워둠
indicators.removeAll()
for _ in 0..<numberOfPages { // 상단에 페이지수가 정해지면 해당 메서드가 실행되기에 중요한 부분
let indicator = UIView()
indicator.layer.cornerRadius = 2
indicator.backgroundColor = defaultColor
indicator.translatesAutoresizingMaskIntoConstraints = false
indicator.widthAnchor.constraint(equalToConstant: indicatorWidth).isActive = true // 미리 정의한 값으로 설정
indicator.heightAnchor.constraint(equalToConstant: indicatorHeight).isActive = true // 위와 동일
stackView.addArrangedSubview(indicator) // 스택뷰의 요소로 넣도록 한다
indicators.append(indicator) // 인디케이터 배열에 추가
}
updateIndicator() // 정의해둔 인디케이터에 관한 애니메이션을 적용
}
updateIndicator은 미리 넣어둔 인디케이터 배열에서 현재페이지의 인덱스를 받아 비교해 대응되는 인디케이터에 대해, 그리고 그 외 인디케이터에도 애니메이션을 통해 색을 적용한다.
private func updateIndicator() {
for (index, indicator) in indicators.enumerated() {
if index == currentPage {
UIView.animate(withDuration: 0.3) {
indicator.backgroundColor = self.selectedColor
}
}
else {
UIView.animate(withDuration: 0.3) {
indicator.backgroundColor = self.defaultColor
}
}
}
}
이 모든 함수가 잘 맞물리도록 해주는 부분이 상단에 있는 코드들인데
private let stackView = UIStackView() // 인디케이터들을 일정 간격 띄우기에 좋은 컴포넌트
private var indicators = [UIView]() // 인디케이터들을 각각 관리해야하기에 배열에 담아야함
private let indicatorWidth: CGFloat = 20 // 미리 정의해둔 인디케이터 너비
private let indicatorHeight: CGFloat = 3 // 미리 정의해둔 인디케이터 높이
private let spacing: CGFloat = 2 // 인디케이터들끼리의 간격
private let selectedColor = UIColor.white // 선택되면 흰색으로
private let defaultColor = UIColor.white.withAlphaComponent(0.3) // 선택되지 못하면 불투명하게
var currentPage: Int = 0 {
didSet { // 현재 페이지가 새롭게 설정된다면 updateIndicator를 실행할 수 있도록.
updateIndicator() // 현재 페이지에 맞게 각 인디케이터를 업데이트
}
}
var numberOfPages: Int = 3 {
didSet { // 총 페이지수가 새롭게 설정된다면 setupIndicators를 실행할 수 있도록.
setupIndicators() // 페이지 수에 맞는 인디케이터로 다시 세팅
}
}
이를 이미지 슬라이더에서 결국 호출하면서 갖고 있는 이미지 수를 알려줘야 numberOfPages에 이어 세팅이 될텐데
private lazy var pageControl: CustomPageControl = {
let control = CustomPageControl()
control.numberOfPages = images.count
control.translatesAutoresizingMaskIntoConstraints = false
return control
}()
이렇게 이미지 슬라이더 측에서 인디케이터를 만들면서 가지고 있는 image들의 개수를 설정하도록 한다. 아까 보았듯 해당 이미지들의 개수가 페이지 수가 되어 인디케이터를 설정하도록 한다.
이 페이지엔 위에 구현된 부분이 viewController안에 일부 viewController로서 자리를 잡아 didMove같은 메서드를 써야했는데, 메인 컨텐츠는 아니니 생략했다. 커스텀으로 어느 컴포넌트를 만들어야할때 개인적으로 벽에 부딪히는 기분이다. 당장 필요한 UI 요소들은 무엇이며 목표 컴포넌트의 컨셉을 잘 수행하기 위해 어느 매커니즘을 따라야 하고 그걸 어떻게 구현하는지 생각할줄 알아야하는데 아직 생각할줄도 모르며 생각한다해도 그걸 어떤식으로 구현해야할지 감이 잡히질 않는 상태다. 차근차근 정말 쉬운 커스텀 컴포넌트부터 직접 도움없이 만들기 시작해 점점 실력을 늘려야겠다고 생각했다.
이번에 면담 및 문제 해결을 도와주신 튜터님도 AI의 과도한 사용은 지양했고, 나 또한 지금 애먹는 이유가 너무 AI 의존도가 높아서구나 싶어 스스로 도움없이 구현할 수 있도록 노력해야겠다는 생각이다.
'iOS > Swift' 카테고리의 다른 글
[TIL / 25.03.12] Alamofire Interceptor에 관한 문제 해결 (0) | 2025.03.12 |
---|---|
[TIL / 25.03.11] 에러 핸들링(Error Handling) (2) | 2025.03.11 |
[TIL / 25.02.25] 자료구조, 메모리 구조, ARC에 대해 간략하게! (0) | 2025.02.25 |
[TIL / 25.02.21] SkeletonView 적용 및 생명주기 관련 문제 해결 (0) | 2025.02.21 |
[TIL / 25.02.17] 뷰 맨 앞으로 보내기 (0) | 2025.02.17 |