안녕하세요.
세그먼트 개념 탐구 포스트#1에서는 세그먼트를 사용하는 방법을 소개했습니다.
이번 포스트는 세그먼트를 클릭할 때 전환되는 언더바와 UISegmentedControl의 items에 문자열들을 보여주는 label을 segmented control안에서 인스턴스로 찾아 각각의 레이블 안에 둥근 알림 뷰를 추가하도록 segmented control을 커스텀하려고 합니다.
깃허브 프로젝트 코드 링크입니다. 세그먼트를 누르면 빨간 아이콘이 사라지고, 특정한 경우에 특정 세그먼트에 빨간 아이콘을 생성하도록 호출하는 함수를 구현했습니다.
final class RedIconBasedUnderbarSegmentedControl: UISegmentedControl {
private lazy var underbar: UIView = makeUnderbar()
private lazy var underbarWidth: CGFloat? = bounds.size.width / CGFloat(numberOfSegments)
private lazy var redIcons: [UIView] = {
return (0..<numberOfSegments).map { _ in
return {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.layer.cornerRadius = redIconWidth/2
$0.layer.backgroundColor = UIColor.red.cgColor
$0.alpha = 1
return $0
}(UIView(frame: .zero))
}
}()
override var selectedSegmentIndex: Int {
didSet {
hideSpecificRedIcon(with: selectedSegmentIndex)
// TODO: - 헨들러를 통해 알림 확인했다는 로직 외부에서 구현!!
}
}
private var underbarInfo: UnderbarInfo
private var isFirstSettingDone = false
}
RedIconBasedUnderbarSegmentedControl 클래스는 주황색 언더바 뷰 프로퍼티, 언더바 길이, 빨간아이콘 등이 프로퍼티로 있습니다. 세그먼트 컨트롤은 신기하게 다른 세그먼트를 클릭할 때마다 layoutSubviews()가 호출됩니다. 세그먼트의 선택 상태가 달라지면서 내부적으로 뷰의 레이아웃이 변경되서 layoutSubviews()가 호출되는 것 같습니다.
기본적으로 제공되는 배경색, divider를 제거합니다.
override func layoutSubviews() {
super.layoutSubviews()
let underBarLeadingSpacing = CGFloat(selectedSegmentIndex) * (underbarWidth ?? 50)
UIView.animate(withDuration: 0.27, delay: 0, options: .curveEaseOut, animations: {
self.underbar.transform = .init(translationX: underBarLeadingSpacing, y: 0)
})
}
layoutSubviews()는 다른 세그먼트를 클릭했을 때도 호출이 되고 이점을 이용해서 underbar(오랜지색) 의 위치를 변경합니다.
이렇게 하면 세그먼트가 클릭될 때마다 layoutSubviews()가 호출되고 이때 언더바의 위치도 같이 변경됩니다.
각각의 세그먼트(알림, 공지사항, what's new)에 대해서 서로 다른 pageViewController를 사용해서 세부적인 3개의 화면을 구현하는 것도 하나의 방법이고 컬랙션 뷰를 사용해서 서로 다른 cell을 보여주는 것도 좋은 것 같습니다.
위 내용을 기반으로 사용자가 보지 않은 세그먼트의 화면에 새로운 소식이 추가됬다면, 빨간 아이콘을 통해 사용자에게 확인하라는 표시가 필요했습니다.
그래서 세그먼트 컨트롤 안에 보여지는 레이블의 인스턴스를 얻고 싶었습니다. 그러나 UISegmentedControl은 지원을 하지 않습니다. (뷰 계층 구조를 살펴보면 저렇게 UISegment 뷰 안에 UISegmentedLabel이라는 UILabel이 존재하는데..)
그래도 이 객체는 3개의 이미지뷰가있고, 3개의 세그먼트가 있습니다. 그 안에 UILabel들이 존재합니다. 그래서 저는 이 3개의 인스턴스를 직접 얻었습니다... UIView는 여러 개의 subview를 갖습니다: ) 이 subviews중에 UILabel타입이 있다면 그게 제가 원하는 인스턴스입니다.
class RedIconBasedUnderbarSegmentedControl {
...
func setRedIconInSegmentedViews() {
let titles = (0..<numberOfSegments).map { // 1.
titleForSegment(at: $0)
}
let segmentedTitleLabels = subviews // 2.
.compactMap { subview in
subview.subviews.compactMap { $0 as? UILabel } // 3.
}
.flatMap { $0 } // 4.
.sorted { // 5.
guard
let idx1 = titles.firstIndex(of: $0.text ?? ""),
let idx2 = titles.firstIndex(of: $1.text ?? "")
else { return false }
return idx1 < idx2
}
}
}
1. 세그먼트 컨트롤 안에서 찾은 레이블 인스턴스들은 순서가 뒤죽박죽입니다. 그래서 "알림", 공지사항", "what's new" 이 3개의 순서대로 정렬하기 위해 각각 세그먼트에 위치한 String타입의 title을 받아왔습니다
2. 위 뷰 계층구조 그림에서 봣듯이 각각의 subview 중에서,
3. UILabel이 있는지를 찾아야합니다.
4. 그렇게 각각의 뷰가 subview가 없다면 빈 배열, 있다면 레이블이 있는 배열이 반환됩니다. 이 정보들을 1차원 배열로 flat하기위해 flatMap을 사용했습니다.
5. 1에서 언급했듯 인스턴스들은 실제 세그먼트 순서 관계없이 있습니다. 그래서 titleForSegment(at:)를 통해 얻어온 title 순서에 따라서 label들을 정렬했습니다!!!!!!
이렇게하면 각각의 세그먼트에 존재하는 레이블들을 순차적으로 segmentedTitleLabels: [UILabel] 참조값들을 담을 수 있습니다+_+
class RedIconBasedUnderbarSegmentedControl {
...
func setRedIconInSegmentedViews() {
...
segmentedTitleLabels.enumerated().forEach {
let redIcon = redIcons[$0]
$1.addSubview(redIcon)
NSLayoutConstraint.activate([
redIcon.widthAnchor.constraint(equalToConstant: redIconWidth),
redIcon.heightAnchor.constraint(equalToConstant: redIconWidth),
redIcon.topAnchor.constraint(equalTo: $1.topAnchor),
redIcon.trailingAnchor.constraint(equalTo: $1.trailingAnchor, constant: redIconWidth*2)])
}
}
}
원하는 레이블 인스턴스들을 모두 얻었습니다. layoutSubviews시점에는 frame이 명확하게 지정되니, 이 시점에 딱 한번 bool 체크를 통해 레이블 안에 빨간 아이콘을 추가하도록 했습니다. 빨간 아이콘들은 특정 위치의 세그먼트 확인을 해야한다면, 빨간 아이콘 등장, 터치시 사라짐 되도록 구현을 했습니다.
다른 방법이 있다면 댓글로 알려주시면 감사합니다.
깃허브 프로젝트 코드:
references:
https://ios-development.tistory.com/963?category=899471