Swift: Side Menu from Scratch (Swift 5, Xcode 12, UIKit, 2022) - iOS Development
extension ContainerViewController: HomeViewControllerDelegate {
func didTapMenuButton() {
toggleMenu(completion: nil)
}
private func toggleMenu(completion: (() -> Void)?) {
switch menuState {
case .closed:
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut) { [weak self] in
guard let self = self else { return }
self.navVC?.view.frame.origin.x = self.homeVC.view.frame.size.width - 100
} completion: { [weak self] done in
if done {
self?.menuState = .opened
}
}
case .opened:
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut) { [weak self] in
guard let self = self else { return }
self.navVC?.view.frame.origin.x = 0
} completion: { [weak self] done in
if done {
self?.menuState = .closed
DispatchQueue.main.async {
completion?()
}
}
}
}
}
}
menuState
로 저장, 버튼을 토글하면서 사이드바를 펼치고 닫는 애니메이션 효과를 구현한 toggleMenu
extension ContainerViewController: SideMenuViewControllerDelegate {
func didSelect(menuItem: SideMenuViewController.SideMenuOptions) {
toggleMenu(completion: nil)
switch menuItem {
case .home:
resetToHome()
case .settings: addVC(to: SettingsViewController())
case .info: addVC(to: InfoViewController())
case .appRating:
break
case .shareApp:
break
}
}
private func addVC(to VC: UIViewController) {
selectedVC = VC
homeVC.addChild(VC)
homeVC.view.addSubview(VC.view)
VC.view.frame = view.bounds
VC.didMove(toParent: homeVC)
homeVC.title = VC.title
}
private func resetToHome() {
guard let selectedVC = selectedVC else { return }
selectedVC.view.removeFromSuperview()
selectedVC.didMove(toParent: nil)
self.selectedVC = nil
homeVC.title = "Home"
}
}
addVC
, resetToHome
이라는 별도의 함수를 통해 selectedVC
라는 뷰 컨트롤러 옵셔널 값을 전역으로 사용해 바꿔끼우기homeVC
상단에 addSubview
로 해당 뷰 컨트롤러의 뷰 자체를 추가하거나 해당 뷰 컨트롤러의 뷰를 슈퍼 뷰에서 삭제하는 방법으로 '이동하는 것처럼' 보이게 구현addChild
메소드와 didMove
를 함께 사용private func addChildVC() {
addChild(menuVC)
view.addSubview(menuVC.view)
menuVC.didMove(toParent: self)
menuVC.delegate = self
let navVC = UINavigationController(rootViewController: homeVC)
homeVC.delegate = self
addChild(navVC)
view.addSubview(navVC.view)
navVC.didMove(toParent: self)
self.navVC = navVC
}
class ContainerViewController: UIViewController {
enum SideMenuState {
case opened
case closed
}
private var menuState = SideMenuState.closed
private let menuVC = SideMenuViewController()
private let homeVC = HomeViewController()
private var navVC: UINavigationController?
private var selectedVC: UIViewController?
override func viewDidLoad() {
super.viewDidLoad()
setUI()
addChildVC()
}
private func setUI() {
view.backgroundColor = .systemRed
}
private func addChildVC() {
addChild(menuVC)
view.addSubview(menuVC.view)
menuVC.didMove(toParent: self)
menuVC.delegate = self
let navVC = UINavigationController(rootViewController: homeVC)
homeVC.delegate = self
addChild(navVC)
view.addSubview(navVC.view)
navVC.didMove(toParent: self)
self.navVC = navVC
}
}
extension ContainerViewController: SideMenuViewControllerDelegate {
func didSelect(menuItem: SideMenuViewController.SideMenuOptions) {
toggleMenu(completion: nil)
switch menuItem {
case .home:
resetToHome()
case .settings: addVC(to: SettingsViewController())
case .info: addVC(to: InfoViewController())
case .appRating:
break
case .shareApp:
break
}
}
private func addVC(to VC: UIViewController) {
selectedVC = VC
homeVC.addChild(VC)
homeVC.view.addSubview(VC.view)
VC.view.frame = view.bounds
VC.didMove(toParent: homeVC)
homeVC.title = VC.title
}
private func resetToHome() {
guard let selectedVC = selectedVC else { return }
selectedVC.view.removeFromSuperview()
selectedVC.didMove(toParent: nil)
self.selectedVC = nil
homeVC.title = "Home"
}
}
extension ContainerViewController: HomeViewControllerDelegate {
func didTapMenuButton() {
toggleMenu(completion: nil)
}
private func toggleMenu(completion: (() -> Void)?) {
switch menuState {
case .closed:
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut) { [weak self] in
guard let self = self else { return }
self.navVC?.view.frame.origin.x = self.homeVC.view.frame.size.width - 100
} completion: { [weak self] done in
if done {
self?.menuState = .opened
}
}
case .opened:
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseInOut) { [weak self] in
guard let self = self else { return }
self.navVC?.view.frame.origin.x = 0
} completion: { [weak self] done in
if done {
self?.menuState = .closed
DispatchQueue.main.async {
completion?()
}
}
}
}
}
}
selectedVC
의 일종으로 해도 될 것 같은데, 이는 즉 addVC
함수에서 선택한 뷰 컨트롤러의 뷰 자체를 homeVC
에 넣거나 빼는 게 아니라 homeVC
자체를 특정 뷰 컨트롤러로 바꿔버리는 방법임addChild
와 removeFromParent
등 자식/부모 관계를 설정하는 뷰 컨트롤러 인스턴스 메소드를 계속해서 사용해야 하기 때문에 UI 상으로 보여지는 데에도 레이턴시가 존재할 것protocol HomeViewControllerDelegate: AnyObject {
func didTapMenuButton()
}
class HomeViewController: UIViewController {
weak var delegate: HomeViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
setUI()
}
private func setUI() {
view.backgroundColor = .systemRed
title = "Home"
navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "list.dash"), style: .done, target: self, action: #selector(didTapMenuButton))
}
@objc private func didTapMenuButton() {
delegate?.didTapMenuButton()
}
}
protocol SideMenuViewControllerDelegate: AnyObject {
func didSelect(menuItem: SideMenuViewController.SideMenuOptions)
}
class SideMenuViewController: UIViewController {
enum SideMenuOptions: String, CaseIterable {
case home = "Home"
case settings = "Settings"
case info = "Information"
case appRating = "App Rating"
case shareApp = "Share App"
var imageName: String {
switch self {
case .home:
return "house"
case .settings:
return "gear"
case .info:
return "airplane"
case .appRating:
return "star"
case .shareApp:
return "message"
}
}
}
private lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.delegate = self
tableView.dataSource = self
tableView.backgroundColor = .systemGray
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tableViewCell")
return tableView
}()
weak var delegate: SideMenuViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
setUI()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = CGRect(x: 0, y: view.safeAreaInsets.top, width: view.bounds.size.width, height: view.bounds.size.height)
}
private func setUI() {
view.backgroundColor = .systemGray
view.addSubview(tableView)
}
}
extension SideMenuViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let model = SideMenuOptions.allCases[indexPath.row]
delegate?.didSelect(menuItem: model)
}
}
extension SideMenuViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return SideMenuOptions.allCases.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
let model = SideMenuOptions.allCases[indexPath.row]
cell.textLabel?.text = model.rawValue
cell.textLabel?.textColor = .white
cell.imageView?.image = UIImage(systemName: model.imageName)?.withTintColor(.white, renderingMode: .alwaysOriginal)
cell.backgroundColor = .systemGray
cell.contentView.backgroundColor = .systemGray
return cell
}
}
사이드 바의 선택 메뉴에 따라 현재 보여주는 뷰를 결정하는 게 키 포인트인데, 강의에서 보여주는 컨테이너 뷰가 가지고 있는 첫 번째 자식 뷰
homeVC
에 선택 뷰 컨트롤러의 뷰 자체를 추가/삭제하는 방법은 위험할 수도 있겠다는 생각이 들었다.