1. 미리보는 결과
옆에서 펼쳐지는 메뉴를 지금껏 햄버거바라고 했는데,, 찾아보니깐 사이드 메뉴라고 하더라구요?!
이를 라이브러리를 써서 구현하는 방법도 있으나 이 방법은 이미지난 포스트에서 다뤘던 것처럼 상단 탭바에서도 썼기에,,
직접 구현하는 방법은 많이 어려운가해서 찾아보고 있었는데
https://world-of-larooly.tistory.com/88
Swift Side Menu UI 만들기 (with Code)
이번에 만들 UI는 명칭이 좀 다양한데요. 사이드바, 메뉴바, 햄버거 메뉴, 슬라이드바 등등 부르는 이름이 다양한데 일단 편의상 사이드 메뉴(Side Menu) 라고 부르겠습니다. 또한 만드는 방법이 다
world-of-larooly.tistory.com
요 선생님 코드 참고해서 만들어보았습니다.
솔직히 보고 얼추 이해는 되는데 아직 모르는 부분이 꽤 많고, 저는 모든 UI를 스토리보드 없이 코드로만 구현하는 방식을 선호해서 이 부분은 어떻게 대치를 해야하나 하다가 결국 오늘도 Claude의 도움을 받고 말았습니다...
일단 제 프로젝트는 ViewController로 모든걸 해결하진 않고, View, ViewController, ViewModel 간 역할을 나눠 구성되어있어서 저 블로그 내 코드도 분리시켰습니다. (클로드를 통해..)
2. 구현
class SideMenuView: UIView {
// 사이드메뉴를 펼치고 접을때 필요한 그림자뷰, 메뉴뷰
private var shadowView: UIView!
private var menuView: UIView!
// isExpanded로 메뉴의 펼쳐짐 상태 관리, 그 상태에 따라 바뀔 Constraint
private var trailingConstraint: NSLayoutConstraint!
private var isExpanded: Bool = false
// 터치를 감지를 해야하기에 GestureRecognizer 선언
private var tapGesture: UITapGestureRecognizer?
// 디자인 상 메뉴바의 너비
private let menuWidth: CGFloat = 345
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
// 이 뷰를 다른 뷰와 같이 쓸건데 이건 후에 설명
isUserInteractionEnabled = false
}
// 스토리보드 없이 쓸거라 이렇게 처리
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented.")
}
private func setupUI() {
setupShadowView()
setupMenuView() // ShadowView 위에 MenuView를 올리는 구조
setupConstraints()
}
// 그림자 뷰 설정
private func setupShadowView() {
shadowView = UIView(frame: bounds) // SideMenuView의 bound에 맞춰 UIView 생성
shadowView.autoresizingMask = [.flexibleWidth, .flexibleHeight] // 부모 뷰의 너비와 높이에 맞춰 리사이징
shadowView.backgroundColor = .black
shadowView.alpha = 0.0 // 메뉴를 펼치기 전엔 보이면 안되기에 투명하게
addSubview(shadowView)
}
private func setupMenuView() {
menuView = UIView()
menuView.backgroundColor = UIColor(named: "Bg 1")
addSubview(menuView)
}
private func setupConstraints() {
menuView.snp.makeConstraints {
$0.width.equalTo(menuWidth)
$0.top.bottom.equalToSuperview()
}
trailingConstraint = menuView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: menuWidth)
trailingConstraint.isActive = true
}
@objc private func handleTap() {
if isExpanded {
toggleMenu()
}
}
func toggleMenu() {
print("toggleMenu")
isExpanded.toggle()
isUserInteractionEnabled = isExpanded
if isExpanded {
tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
shadowView.addGestureRecognizer(tapGesture!) // 메뉴가 펼쳐지면 탭을 감지할 수 있어야 함.
} else {
if let gesture = tapGesture {
shadowView.removeGestureRecognizer(gesture) // 메뉴가 접히게 되면 기존 존재하던 tapGesture를 없애줘야 함.
}
}
let targetPosition = isExpanded ? 0 : menuWidth // trailing를 걸어둘 수치를 확장 상태에 따라 정함
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: .layoutSubviews) {
self.trailingConstraint.constant = targetPosition // constraint 수치 변경해 이동
self.layoutIfNeeded() // constraint를 변경하는 것이기에 다시 레이아웃하도록
self.shadowView.alpha = self.isExpanded ? 0.6 : 0.0 // 쉐도우 뷰 또한 확장 여부에 따라 투명도 조절
}
}
}
우선 가장 포인트가 될 사이드메뉴 뷰. 원래 뷰컨트롤러도 만들어서 사이드메뉴를 쓰는 뷰컨트롤러가 상속받는 구조를 생각했는데, 그 뷰컨트롤러는 그 뷰를 담고 토글하면 어떻게 처리해줄지에 대한 것만 담을 뿐이라 굳이 필요없겠다고 판단했습니다.
그럼 결국 구조가 SideMenu를 필요로 하는 뷰컨트롤러인 HomeViewController, 그 안에 뷰를 책임지는 HomeView, 동시에 사이드메뉴 UI 관련 처리를 담당하는 SideMenuView가 되겠습니다.
HomeViewController - HomeView, SideMenuView
이제 홈 뷰컨트롤러쪽에서 이를 어떻게 쓰는지 다뤄보자면
view.addSubview(homeView)
homeView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
view.addSubview(sideMenuView)
sideMenuView.snp.makeConstraints {
$0.horizontalEdges.bottom.equalToSuperview()
$0.top.equalTo(view.safeAreaLayoutGuide.snp.top)
}
우선 최상위 뷰에 두 뷰를 서브뷰로 설정해 각자의 터치 이벤트 등에 대한 처리를 명확하게 하면서, sideMenuView를 homeView의 위에 두었습니다. 아까 SideMenuView에서 ShadowView의 UserInteractionEnabled를 false로 설정한 이유도 이것에 있습니다.
앱 실행 후 홈에 들어오면 홈 화면에 있는 버튼들과 상호작용해야하기에 쉐도우뷰는 잠시 비활성화하도록 한겁니다.
@objc func handleMenuToggle() {
sideMenuView.toggleMenu()
}
extension HomeViewController: HomeViewDelegate {
func homeViewDidTapSideMenu() {
print("sideMenu Tap")
handleMenuToggle()
}
}
protocol HomeViewDelegate: AnyObject {
func homeViewDidTapSideMenu()
func homeViewDidTapPost()
func didSearch(_ query: String)
func didSelectTag(_ tag: String)
}
// HomeView.swift 중
private func setupActions() {
SideMenuButton.addTarget(self, action: #selector(didTapSideMenuButton), for: .touchUpInside)
}
@objc private func didTapSideMenuButton() {
delegate?.homeViewDidTapSideMenu()
}
홈뷰에서 넣어두었던 햄버거바 버튼에 대한 메서드를 뷰컨트롤러 측에서 담당할 수 있게 homeViewDelegate를 이용했었는데, 이에 대한 내용을 구현한 코드입니다.
결국 사이드메뉴 뷰 안에서 정의된 애니메이션 관련 코드를 호출하도록 하여 isExpanded의 값에 따라 메뉴를 펼치고 접게 됩니다.
블로그에 있는 코드 모르는 부분은 전부 찾아보고 클로드에 적어서 파악 후에, 내 코드 구조에 맞추는 과정이 꽤 까다로웠다.
각 코드의 역할을 분류하고 정리했어야 했고, 결국 오늘도 AI의 주도하에 진행되긴 했다.
스스로 UI 관련 응용력이 부족하다고 느끼는데, 실력을 키워 이런 커스텀 컴포넌트 정도는 알아서 80%이상 구현할 수 있게 되어야 한다고 생각한다.
전엔 저런 코드를 AI에 물어봐 설명시켜도 이해를 잘 못했는데 이해도 자체는 전보다 훨씬 좋아져서 실력이 늘고 있긴하네..!라고 생각하게 된다. 화이탱
'iOS > Swift' 카테고리의 다른 글
[TIL / 25.02.14] 열거형 Enum을 이용한 메뉴 구성 (2) | 2025.02.14 |
---|---|
[TIL / 25.02.13] 다중 섹션 및 헤더가 포함된 사이드 메뉴 구현 (0) | 2025.02.13 |
[TIL / 25.02.10] 헤더를 이용한 테이블뷰를 구성, 셀과 헤더 높이 자동 계산 (4) | 2025.02.10 |
[TIL / 25.02.07] 라이브러리 사용해서 상단 탭바 구현하기 2 (0) | 2025.02.07 |
[TIL / 25.02.06] 라이브러리 사용해서 상단 탭바 구현하기 1 (5) | 2025.02.06 |