이번엔 정말 한참 잡고 있었던 것 같은데 ...
1. 리팩토링한 부분
기존 홈 내 알바생이 본인의 근무지를 볼 수 있었고 셀을 탭하면 확장 및 축소가 되어 급여에 대한 상세한 요소들을 볼 수 있어야 했음
요런 느낌으로 했어야 했는데 리뉴얼 전 프로젝트에서는 스택뷰로 하게 되면 계속 제약조건 경고와 함께 UI 관련 이슈가 끊이질 않고 원하는대로 되지도 않아서 데드라인이 얼마 남지 않았기에 해당 방식으로 진행함.
func setConstraints() {
containerView.snp.makeConstraints {
$0.top.equalToSuperview().inset(4)
$0.directionalHorizontalEdges.equalToSuperview().inset(16)
$0.bottom.equalToSuperview().inset(8).priority(.high)
}
headerView.snp.makeConstraints {
$0.top.equalToSuperview()
$0.directionalHorizontalEdges.equalToSuperview()
}
// 생략 ...
detailStackView.snp.makeConstraints {
$0.top.equalTo(headerView.snp.bottom).offset(8)
$0.directionalHorizontalEdges.equalToSuperview().inset(24)
}
expandToggleImageView.snp.makeConstraints {
$0.centerX.equalToSuperview()
$0.width.equalTo(22)
$0.height.equalTo(16)
$0.bottom.equalToSuperview().inset(8)
}
expandToggleImageView.snp.prepareConstraints {
self.expandToggleTopToHeaderConstraint = $0.top.equalTo(headerView.snp.bottom).offset(8).constraint
self.expandToggleTopToDetailConstraint = $0.top.equalTo(detailStackView.snp.bottom).offset(8).constraint
}
expandToggleTopToHeaderConstraint?.activate()
expandToggleTopToDetailConstraint?.deactivate()
}
최하단에 위치한 화살표 아이콘의 이미지뷰의 상단을 확장되었을 때 붙여야할 컴포넌트의 하단과 축소되었을 때 붙여야할 컴포넌트의 하단을 기준으로 두 개의 제약조건 프로퍼티를 만들어두고 초기엔 축소되어있어야하는 폼이니 해당 제약조건만 활성화함.
private func updateExpandToggleConstraints(isExpanded: Bool) {
print("제약조건 업데이트: \(isExpanded)")
if isExpanded {
expandToggleTopToHeaderConstraint?.deactivate()
expandToggleTopToDetailConstraint?.activate()
} else {
expandToggleTopToDetailConstraint?.deactivate()
expandToggleTopToHeaderConstraint?.activate()
}
if isExpanded {
// 펼칠 때는 애니메이션 적용
UIView.animate(withDuration: 0.3) {
self.layoutIfNeeded()
self.contentView.layoutIfNeeded()
self.superview?.layoutIfNeeded()
}
} else {
// 접을 때는 애니메이션 없이 즉시 적용
self.layoutIfNeeded()
self.contentView.layoutIfNeeded()
self.superview?.layoutIfNeeded()
}
}
func toggleDetailView(isExpanded: Bool) {
print("toggleDetailView 호출: \(isExpanded)")
detailStackView.isHidden = !isExpanded
expandToggleImageView.image = isExpanded ? .chevronUnfolded : .chevronFolded
updateExpandToggleConstraints(isExpanded: isExpanded)
}
확장 여부에 따라 각 제약조건의 활성화를 토글하고 중간에 있던 급여 표도 isHidden 처리함
당시 해당 방식엔 여러모로 급박하게 해서 애니메이션이 정말 부자연스러운데도 넘어갈 수 밖에 없었음
이번 리뉴얼에서 UIStackView에서 요소의 추가 혹은 삭제에 따른 자연스러운 애니메이션을 이용해보자생각해서 구조를 다시 잡음.
// MARK: - setHierarchy
func setHierarchy() {
contentView.addSubviews(containerView)
containerView.addSubviews(
stackView,
menuButton
)
firstSectionView.addSubviews(
nameLabel,
untilPaydayLabel,
totalEarnedLabel
)
secondSectionView.addSubviews(
dummyView
)
thirdSectionView.addSubviews(
chevronImageView
)
stackView.addArrangedSubviews(
firstSectionView,
secondSectionView,
thirdSectionView
)
}
우선 contentView - containerView - stackView, menuButton - firstSectionView, secondSectionView, thirdSectionView 구조로 구성했고
firstSectionView와 thirdSectionView는 고정 요소 그리고 secondSectionView가 위에서 말했던 급여 표로 가변 요소가 되겠다.
UIStackView는 축과 정렬된 서브뷰들간의 거리를 설정된 토대로 나열하며 내부 요소가 빠지거나 추가될 경우 자연스러운 애니메이션으로 해당 모션을 보여주기에 그림자를 보여주는 카드뷰는 ContainerView로 표현하고 내부에 이 동적인 셀을 만들 수 있도록 해주는 핵심 컴포넌트인 stackView를 넣기로 결정했음
// MARK: - setConstraints
func setConstraints() {
containerView.snp.makeConstraints {
$0.top.equalToSuperview().inset(12)
$0.directionalHorizontalEdges.equalToSuperview().inset(16)
}
stackView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
// 첫 번째 섹션
nameLabel.snp.makeConstraints {
$0.top.equalToSuperview().inset(12)
$0.leading.equalToSuperview().inset(16)
}
untilPaydayLabel.snp.makeConstraints {
$0.top.equalTo(nameLabel.snp.bottom)
$0.leading.equalToSuperview().inset(16)
}
menuButton.snp.makeConstraints {
$0.top.equalToSuperview()
$0.trailing.equalToSuperview().inset(6)
$0.size.equalTo(44)
}
totalEarnedLabel.snp.makeConstraints {
$0.top.equalTo(menuButton.snp.bottom).offset(10)
$0.trailing.equalToSuperview().inset(16)
$0.bottom.equalToSuperview()
}
// 두 번째 섹션
secondSectionView.snp.makeConstraints {
self.secondSectionHeightConstraint = $0.height.equalTo(200).constraint
}
dummyView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
// 세 번째 섹션
chevronImageView.snp.makeConstraints {
$0.top.equalToSuperview().inset(12)
$0.centerX.equalToSuperview()
$0.bottom.equalToSuperview().inset(8)
$0.width.equalTo(22)
$0.height.equalTo(16)
}
containerView.snp.makeConstraints {
$0.bottom.equalToSuperview().inset(12)
}
}
func toggleSecondSection(_ expanded: Bool) {
self.secondSectionHeightConstraint?.update(offset: isExpanded ? 200 : 0)
UIView.animate(withDuration: 0.25) {
self.layoutIfNeeded()
}
}
우선 당시의 문제 코드. 당연히 중간에 있는 서브뷰 secondSectionView의 높이를 가변적으로 조정하면 알아서 스택뷰가 줄여주고 늘려주고 할 줄 알았는데...
2. 첫 번째 트러블
아 왜 이러는데요...
2-1. 첫 번째 트러블 해결
해당 문제는
func toggleSecondSection(_ expanded: Bool) {
self.dummyHeightConstraint?.update(offset: expanded ? 200 : 0)
if let tableView = self.superview as? UITableView {
tableView.beginUpdates()
self.contentView.layoutIfNeeded()
tableView.endUpdates()
}
}
tableView에 있는 beginUpdates와 endUpdates 메서드를 호출해서 해결했습니다..!
tableView 내부 요소들이 막 변해도 명시적으로 그리는 사이클을 돌리게 해서 위같은 상황은 해결
삽입, 삭제 등의 메서드 호출들의 시리즈를 시작한다
앞서 말했던 일련의 작업들을 마무리한다.
이런 맥락이어서 내가 변화를 주었다면 그것들을 테이블뷰한테 제대로 반영해서 보여줘하고 시키는 것 같아요
3-1. 두 번째 트러블
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<SnapKit.LayoutConstraint:0x600002629980@WorkerWorkplaceCell.swift#140 MOUP.CardButton:0x105245c80.top == UITableViewCellContentView:0x10524cf60.top + 12.0>",
"<SnapKit.LayoutConstraint:0x600002605d40@WorkerWorkplaceCell.swift#142 MOUP.CardButton:0x105245c80.bottom == UITableViewCellContentView:0x10524cf60.bottom - 12.0>",
"<SnapKit.LayoutConstraint:0x6000026059e0@WorkerWorkplaceCell.swift#146 UIStackView:0x105247c40.bottom == MOUP.CardButton:0x105245c80.bottom>",
"<SnapKit.LayoutConstraint:0x600002607de0@WorkerWorkplaceCell.swift#161 UIButton:0x1052499c0.top == MOUP.CardButton:0x105245c80.top>",
"<SnapKit.LayoutConstraint:0x60000263bba0@WorkerWorkplaceCell.swift#163 UIButton:0x1052499c0.height == 44.0>",
"<SnapKit.LayoutConstraint:0x60000263b960@WorkerWorkplaceCell.swift#167 UILabel:0x10524b710.top == UIButton:0x1052499c0.bottom + 10.0>",
"<SnapKit.LayoutConstraint:0x60000263bf60@WorkerWorkplaceCell.swift#169 UILabel:0x10524b710.bottom == UIView:0x105247e00.bottom>",
"<SnapKit.LayoutConstraint:0x60000263c0c0@WorkerWorkplaceCell.swift#174 UIView:0x10524bbe0.top == UIView:0x10524ba40.top>",
"<SnapKit.LayoutConstraint:0x60000263c180@WorkerWorkplaceCell.swift#174 UIView:0x10524bbe0.bottom == UIView:0x10524ba40.bottom>",
"<SnapKit.LayoutConstraint:0x60000263c000@WorkerWorkplaceCell.swift#175 UIView:0x10524bbe0.height == 200.0>",
"<SnapKit.LayoutConstraint:0x60000263c240@WorkerWorkplaceCell.swift#180 UIImageView:0x10524c050.top == UIView:0x10524beb0.top + 12.0>",
"<SnapKit.LayoutConstraint:0x60000263c360@WorkerWorkplaceCell.swift#182 UIImageView:0x10524c050.bottom == UIView:0x10524beb0.bottom - 8.0>",
"<SnapKit.LayoutConstraint:0x60000263c3c0@WorkerWorkplaceCell.swift#184 UIImageView:0x10524c050.height == 16.0>",
"<NSLayoutConstraint:0x60000214aee0 'UISV-canvas-connection' V:[UIView:0x10524beb0]-(0)-| (active, names: '|':UIStackView:0x105247c40 )>",
"<NSLayoutConstraint:0x60000214af30 'UISV-spacing' V:[UIView:0x105247e00]-(8)-[UIView:0x10524ba40] (active)>",
"<NSLayoutConstraint:0x60000214af80 'UISV-spacing' V:[UIView:0x10524ba40]-(8)-[UIView:0x10524beb0] (active)>",
"<NSLayoutConstraint:0x60000214b570 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x10524cf60.height == 147 (active)>"
)
Will attempt to recover by breaking constraint
<SnapKit.LayoutConstraint:0x60000263c000@WorkerWorkplaceCell.swift#175 UIView:0x10524bbe0.height == 200.0>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
셀 확장 축소는 정상적으로 되는데 첫 번째 확장에서만 해당 제약조건 warning이 발생한다....(이때는 스택뷰 너 굉장히 다루기 힘든 친구구나..싶긴했는데)
문제는 결국 축소 당시에 tableView는 automaticDimension 기반이기도하고 두번째섹션이 0인 상태에서 높이를 측정후 시스템 상에 올려놓음(UIView-Encapsulated-Layout-Height ~~ height == 147).
다만 바로 그 셀을 탭하고나서 바로 확장해버리면서 그 안에 두번째 섹션뷰가 200만큼의 높이를 가진채로 중간에 끼게 됨. 이렇게 되면서 제약조건 충돌 발생함
147으로 이미 설정해놨는데 중간에 200을 끼워버리니 뭐 어쩌라는건지 시스템에서 문제를 삼게 되는 상황
3-2. 두 번째 트러블 해결
해당 상황은 결국 시스템이 147을 설정해놓고 이거 벗어날라하네? 위험한데? 하는 상황이고 이 상황을 우선순위 설정을 통해 해결함
시스템 상 147과 충돌하는 200만큼의 높이를 설정하는 제약조건 명령문에 priority 설정.
// 두 번째 섹션
dummyView.snp.makeConstraints {
$0.edges.equalToSuperview()
dummyHeightConstraint = $0.height.equalTo(0).priority(.medium).constraint
}
알아보니 시스템에서의 제약조건 Encapsulated~~ 이 부분은 required로 1000만큼의 우선도를 가짐.
위 코드에서 priority 없을 땐 기본값으로 required를 가져서 동등한 우선도를 가지게 되는데, 이때 서로 같은 우선도를 가지기에 충돌하게 돼서 경고를 띄우는 것 같음
이제부턴 꼭 맞다싶은건 아니지만 해결은 했고 나름의 해석을 한 부분인데, priority 설정 시 .low로 했을 땐 250의 우선도를 가지게 됐고 셀의 높이가 바뀌지 않았음. 시스템에서 훨씬 우선도가 높은 기존 설정을 선택을 하고 해당 200 설정은 일단 배제한 것 같음
다만 .medium(500), .high(750)을 설정했을 땐 경고 없이 제대로 반영됨.
처음엔 도무지 이해가 안갔음, 시스템에서 정한 우선도가 훨씬 높은데 뭘 해도 안통해야 하는 거 아닌가? 했는데 4개의 ai 모델을 통해 물어보니 시스템은 내부적으로 양 측 제약조건을 최대한 챙기는 방향으로 진행한다.
이렇게 되면 아마 low의 경우 우선도가 많이 낮아서 크게 사이클 상에서 고려하지 않았던 것 같고 medium과 high는 비교적 높으니 문제되지 않는 선에서 외부에서 커버를 했던 것 같다는 해석이다.
4. 리팩토링 전 후 결과
이게 전인데 확실히 펼칠때 좌측 상단에서 우측 하단으로 퍼져나가는게 부자연스러웠음.
위에서 리뉴얼 이후 바꾼 버전엔
위에서 아래로 펼쳐지는 애니메이션이 연출됨. 훨씬 자연스러워졌음.
끝..
UI/UX에 관심이 많고 최대한 챙겨가려고 하다가 구조를 바꾸다보니 시간이 많이 걸렸는데, 동적 높이로 보여주게 된다면 생각보다 고려해야 될 점들이 많은 것 같다. 특히나 시스템에서 미리 계산해서 저장해두었다가 이를 깨는 설정이 있었을 때 충돌이 난 경우인데, 처음으로 해결해보았고 100% 흡수하진 못한게 아쉽다..
priority가 생각보다 중요함을 또 느꼈고 어디선 huggingCompression 등등 다른 키워드들도 함께 보였는데 이도 공부해봐야 할 것 같다.
tableView에 begin, endUpdates를 처음 알게 되었는데 이런 컴포넌트마다 가지는 사이클 관련된 메서드들도 어느정도 알아두어야 할 것 같다.
'iOS > Swift' 카테고리의 다른 글
[25.08.04] MOUP 트러블슈팅 - .xcconfig 내 url 설정 시 주의사항 (5) | 2025.08.05 |
---|---|
[25.07.31] MOUP 트러블슈팅 - tableHeaderView 레이아웃 제약 경고 (2) | 2025.07.31 |
[25.06.24] MOUP 트러블슈팅 - Listener와 Rx의 timeout (0) | 2025.06.24 |
[TIL / 25.05.27] 날씨 앱 main 페이지 구조 트러블슈팅 (5) | 2025.05.27 |
[TIL / 25.05.18] 의존성 주입 담당 DIContainer를 처음 적용해보았습니다 (4) | 2025.05.18 |