SwiftUI만 하다가 UIKit을 하게될 일이 생겼는데
요구사항중하나가
뷰를 크기에 맞게 여러줄로 표현해주는 뷰에요
테그를 표현하는 뷰같은 곳에 많이 쓰이는 UI로 알고있어요
결과물 부터 보시죠!
네 이런뷰입니다..
SwiftUI로도 이뷰는 어떻게 만들어야할까 깊은 고뇌를 해봤습니다...
스유는 레이아웃 건드는게 좀 어렵더라구요 🤯
우선 UIKit으로 개발해야하니깐 UIKit관점에서 쉬운방법으로 구현해봤습니다!
이런뷰를 구현할때 많은 방법이 존재하겠지만
단순한 방법으로
뷰의 frame을 계산해서 구현하는 방식을 선택했어요
전체코드는 아래와같아요!
class MultiLineTagView: UIView {
private let horizontalSpacing: CGFloat
private let verticalSpacing: CGFloat
private let rowHeight: CGFloat
private var intrinsicHeight: CGFloat = 0
private let horizontalPadding: CGFloat
private var labels: [UILabel] = []
init(
horizontalSpacing: CGFloat = 0,
verticalSpacing: CGFloat = 0,
rowHeight: CGFloat,
horizontalPadding: CGFloat = 0
) {
self.horizontalSpacing = horizontalSpacing
self.verticalSpacing = verticalSpacing
self.rowHeight = rowHeight
self.horizontalPadding = horizontalPadding
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// 상속해서 사용할 label의 속성을 지정해줍니다
func applyAttribute(label: UILabel) {
label.textColor = .black
label.layer.borderWidth = 1
label.layer.borderColor = UIColor.gray.cgColor
label.layer.cornerRadius = 10
}
final func setTag(words: [String]) {
for word in words {
let label = UILabel()
label.text = word
label.textAlignment = .center
applyAttribute(label: label)
addSubview(label)
label.frame.size.width = label.intrinsicContentSize.width + horizontalPadding * 2
label.frame.size.height = rowHeight
labels.append(label)
}
}
final private func setupLayout() {
var currentX: CGFloat = 0
var currentY: CGFloat = 0
self.labels.forEach { label in
if currentX + label.frame.width > bounds.width {
// 다음행으로 이동
currentX = 0
currentY += rowHeight + verticalSpacing
}
label.frame.origin = CGPoint(x: currentX, y: currentY)
currentX += label.frame.width + horizontalSpacing
}
intrinsicHeight = currentY + rowHeight
invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.height = intrinsicHeight
return size
}
override func layoutSubviews() {
super.layoutSubviews()
setupLayout()
}
}
부분별로 살펴보면
final func setTag(words: [String]) {
for word in words {
let label = UILabel()
label.text = word
label.textAlignment = .center
applyAttribute(label: label)
addSubview(label)
label.frame.size.width = label.intrinsicContentSize.width + horizontalPadding * 2
label.frame.size.height = rowHeight
labels.append(label)
}
}
이부분은 원하는 단어를 넣으면 label을 생성해서 뷰에 추가하는 것까지의 로직이에요
func applyAttribute(label: UILabel) {
label.textColor = .black
label.layer.borderWidth = 1
label.layer.borderColor = UIColor.gray.cgColor
label.layer.cornerRadius = 10
}
이부분은 상속해서사용하도록 구현해놨고
label의 속성을 정의해주면
만들어질때 적용돼서 원하는 디자인을 만들 수 있어요
final private func setupLayout() {
var currentX: CGFloat = 0
var currentY: CGFloat = 0
self.labels.forEach { label in
if currentX + label.frame.width > bounds.width {
// 다음행으로 이동
currentX = 0
currentY += rowHeight + verticalSpacing
}
label.frame.origin = CGPoint(x: currentX, y: currentY)
currentX += label.frame.width + horizontalSpacing
}
intrinsicHeight = currentY + rowHeight
invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.height = intrinsicHeight
return size
}
override func layoutSubviews() {
super.layoutSubviews()
setupLayout()
}
현재x, y좌표를 계산하는 로직이에요
label이 추가됬을때 현재뷰 width를 넘어간다
그러면 y축으로 height과 spacing 만큼 이동시켜서 그리도록 하고
intrinsicHeight를 재계산합니다
이 값은 뷰와 label이 쌓인 뷰의 높이가 같도록 intrinsicContentSize를 계산된 높이를 지정하는데 사용됩니다.
(= 이작업을통해 회색배경만큼의 뷰만 딱 그려집니다 즉, 슈퍼뷰의 크기가 정해집니다)
또한 viewDidLoad 시점이라던가 init시점에 레이아웃을 설정한다면
원치않는 사이즈와 UI가 그려지게돼요
layoutSubviews를 이용해서 레이아웃 설정작업을 해주도록 구현했어요
#Preview {
let tagView = MultiLineTagView(
horizontalSpacing: 8,
verticalSpacing: 8,
rowHeight: 30,
horizontalPadding: 10
)
tagView.setTag(words: [
"NSiOS", "Enes", "가나다라", "abcdefg",
"안녕하세요", "오늘은 9월 30일 입니다.", "좋은 하루되세요~!"
])
tagView.backgroundColor = .lightGray
return tagView
}
사용은 위의 코드같이 간편하게 정의해서 사용할 수 있어요
또한 상속을 한다면
final class NSTagView: MultiLineTagView {
init() {
super.init(
horizontalSpacing: 6,
verticalSpacing: 6,
rowHeight: 30,
horizontalPadding: 10
)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func applyAttribute(label: UILabel) {
label.textColor = .purple
label.font = .systemFont(ofSize: 15, weight: .medium)
label.layer.cornerRadius = 15
label.layer.masksToBounds = true
label.layer.borderColor = UIColor.purple.cgColor
label.layer.borderWidth = 1
}
}
#Preview {
let tagView = NSTagView()
tagView.setTag(words: [
"NSiOS", "Enes", "가나다라", "abcdefg",
"안녕하세요", "오늘은 9월 30일 입니다.", "좋은 하루되세요~!"
])
tagView.backgroundColor = .lightGray
return tagView
}
간편하게 attribute만 바꿔서 사용할 수 있어요!
😈: 가운데로 정렬시키고 싶은데 안돼나요?
🤯: ... ㅠ
가운데 정렬을 적용해봅시다!
어떤 로직을 적용해볼거냐면 기존 프레임계산을 통해 그리기때문에 동일하게 해볼거에요
기존엔 0부터 width를 더해가며
텍스트의 길이가 뷰의 너비보다 넓다면 행을 변경시켜서 프레임을 이동시키는 식으로 했었는데
가운데정렬이 이와 다른점은
시작지점이 0부터가아니라 뷰의 너비에서 현재줄의 텍스트의 총넓이를 뺀 수치의 반 만큼 이동시키면
그 위치가 가운데정렬을 적용하기위한 시작인 위치가 돼요
아래의 그림을 같이 보면 이해가 쉬워요
이런 로직을 적용해서 위치시켜줄겁니다!
setupLayout코드는 아래와같이 변경했어요
// 추가된 타입
enum TagAlignment {
case leading
case center
}
// 추가된 프로퍼티
private let alignment: TagAlignment
final private func setupLayout() {
var currentX: CGFloat = 0
var currentY: CGFloat = 0
var rowfirstIndex: Int = 0
self.labels.indices.forEach { index in
let label = labels[index]
// 다음행으로 이동
if currentX + label.frame.width > bounds.width {
let lastMaxX = currentX - horizontalSpacing
// 이전 행 위치 재정렬
setupAlignment(
lastX: lastMaxX,
firstIndex: rowfirstIndex,
lastIndex: index-1 // 새로운행이기때문에 이전 인덱스가 들어갑니다
)
// 다음줄 초기화
currentX = 0
currentY += rowHeight + verticalSpacing
rowfirstIndex = index // 새로운 행의 시작은 현재 인덱스
}
label.frame.origin = CGPoint(x: currentX, y: currentY)
currentX += label.frame.width + horizontalSpacing
}
// 마지막줄 재정렬
let lastMaxX = currentX - horizontalSpacing
setupAlignment(
lastX: lastMaxX,
firstIndex: rowfirstIndex,
lastIndex: labels.count - 1
)
intrinsicHeight = currentY + rowHeight
invalidateIntrinsicContentSize()
}
private func setupAlignment(lastX: CGFloat, firstIndex: Int, lastIndex: Int) {
guard lastIndex >= 0 else { return }
let startX: CGFloat = if self.alignment == .center {
(bounds.width - lastX) / 2
} else { 0 }
for i in firstIndex...lastIndex {
var newFrame = labels[i].frame
newFrame.origin.x += startX
labels[i].frame = newFrame
}
}
setupAlignment쪽에서
해당 줄에대한 Label들을 재정렬 해주면서 (view.width - 1줄 text의 총 넓이) / 2 만큼 위치를 이동시켜요
이때 alignment옵션에맞게 leading, center정렬을 받아서 알맞게 적용할 수 있어요
'iyOmSd > Title: Swift' 카테고리의 다른 글
[Swift] TimeZone과 Locale (1) | 2024.01.30 |
---|---|
[Swift] NSTextAttachment, 이미지 텍스트화 (1) | 2023.10.31 |
[Swift] Core Motion (feat. 흔들기 감지센서 개발) (0) | 2023.04.19 |
[Swift] Pulse 네트워크 디버깅 라이브러리 (0) | 2023.03.29 |
[Swift] Tuist 모듈화 응용편 - 모듈화적용후 사용하기 (1) | 2023.01.21 |