Notice
Recent Posts
Recent Comments
Link
관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 2. ContainerView 활용 방법 - UIView를 present, dismiss 트랜지션처럼 구현 방법, layering 본문

iOS 응용 (swift)

[iOS - swift] 2. ContainerView 활용 방법 - UIView를 present, dismiss 트랜지션처럼 구현 방법, layering

jake-kim 2022. 5. 13. 23:27

1. ContainerView 활용 방법 - UINavigationView와 View hierarchy

2. ContainerView 활용 방법 - UIView를 present, dismiss 트랜지션 애니메이션 구현 방법

 

* 1번에서 구현한 뷰 계층 관계: VC1 <  VC2, VC3 < VC1 위에 떠있는 버튼

view hierarchy: VC1 < VC2, VC3 < VC1위에 떠있는 버튼

구현할 present, dismiss 애니메이션

  • ViewController지만, 이전 화면을 덮지 않는 view hierarchy를 사용하고 싶은 경우 해결 (이전 1번 포스팅 글에서의 내용)
  • present하지 않고 단순히 애니메이션 만으로 present, dismiss처럼 트랜잭션을 구현
  • 오토 레이아웃을 사용하면 손쉽게 구현 가능

예제에 사용한 프레임워크

  • 오토레이아웃을 손쉽게 다루기 위해 SnapKit 사용

present, dismiss 애니메이션 구현 아이디어

  • 오토레이아웃 + UIView.animte를 사용하면 손쉽게 뷰가 이동되는 애니메이션이 가능하므로 사용
    self.containerView.snp.remakeConstraints {
      $0.edges.equalToSuperview()
    }

    UIView.animate(
      withDuration: 0.3,
      delay: 0,
      options: .curveEaseInOut,
      animations: { self.view.layoutIfNeeded() },
      completion: { _ in completion() }
    )
  • VC1에서 VC2의 뷰를 present처럼 올라오는 애니메이션을 사용하고 싶은 경우 구조
    • VC2.view의 backgroundColor를 clear로 설정
    • VC2.view 뷰의 레이아웃 -> VC1.view와 동일하도록 (꽉 차도록 설정)
    • VC2.view에 containerView를 하나 만들어서 이 뷰를 show()메소드가 불릴때 위로 올라오도록 구현

present처럼 보여질 VC2 구현

  • 필요한 UI 준비
    • present처럼 보여져야 하므로 containerView
    • 제목을 표시할 titleLabel
    • 버튼 클릭 시 push가 되어야 하므로 button
import UIKit
import SnapKit

class VC2: UIViewController {
  private let containerView: UIView = {
    let view = UIView()
    return view
  }()
  private let titleLabel: UILabel = {
    let label = UILabel()
    label.text = "VC2"
    label.textColor = .black
    return label
  }()
  private let button: UIButton = {
    let button = UIButton()
    button.setTitle("다음 화면 push", for: .normal)
    button.setTitleColor(.systemBlue, for: .normal)
    button.setTitleColor(.blue, for: .highlighted)
    button.addTarget(self, action: #selector(pushVC), for: .touchUpInside)
    return button
  }()
  • viewDidLoad()에서 레이아웃을 작성하지 않고 init에서 작성
    • viewDidLoad가 아닌 init()에서 설정안하면, show()메소드가 호출되었을때 containerView가 아직 addSubview되지 않은 상태라 크래시가 발생
    • containerView의 레이아웃 시작점은 바닥에 위치해야하므로 top이 현재 뷰의 bottom과 동일하게 작성 
  init() {
    super.init(nibName: nil, bundle: nil)
    self.view.backgroundColor = .clear
    self.containerView.backgroundColor = .systemGreen
    
    self.view.addSubview(self.containerView)
    self.containerView.addSubview(self.titleLabel)
    self.containerView.addSubview(self.button)
    
    self.containerView.snp.makeConstraints {
      $0.left.right.equalToSuperview()
      $0.top.equalTo(self.view.snp.bottom).priority(999)
    }
    self.titleLabel.snp.makeConstraints {
      $0.top.equalToSuperview().inset(30)
      $0.centerX.equalToSuperview()
    }
    self.button.snp.makeConstraints {
      $0.top.equalToSuperview().inset(70)
      $0.centerX.equalToSuperview()
    }
    
    // layoutIfNeeded호출 안해줄 경우, 좌측 상단에서 레이아웃이 시작
    self.view.layoutIfNeeded()
  }
  • 버튼을 탭한 경우 푸시
  @objc private func pushVC() {
    let vc = VC3()
    self.navigationController?.pushViewController(vc, animated: true)
  }
  • show와 hide 메소드 정의
    • show - containerView는 바닥에 위치하고 있으므로, show일때는 화면에 꽉차게 설정해놓을 경우 위로 올라오는 애니메이션 동작
    • hide - 화면에 지금 꽉차고 있는 상태이므로, 밑으로 내려가는 애니메이션 동작
  func show(completion: @escaping () -> Void = {}) {
    self.containerView.snp.remakeConstraints {
      $0.edges.equalToSuperview()
    }
    UIView.animate(
      withDuration: 0.3,
      delay: 0,
      options: .curveEaseInOut,
      animations: { self.view.layoutIfNeeded() },
      completion: { _ in completion() }
    )
  }
  
  func hide(completion: @escaping () -> Void = {}) {
    self.containerView.snp.remakeConstraints {
      $0.left.right.equalToSuperview()
      $0.top.equalTo(self.view.snp.bottom).priority(999)
    }
    UIView.animate(
      withDuration: 0.3,
      delay: 0,
      options: .curveEaseInOut,
      animations: { self.view.layoutIfNeeded() },
      completion: { _ in completion() }
    )
  }

사용하는 쪽

  • 사용하는쪽에서는 필요할때만 VC2인스턴스를 생성하고, 다시 뷰를 닫을때는 removeFromSuperview하도록 설계
    • 주의할점은 navigationController는 strong으로 잡아주지 않으면 VC2에서 push할때 navigationController가 nil이 되므로 주의
// ViewController.swift

extension ViewController {
  @objc private func openView() {
    guard self.vc2 == nil else { return }
    let vc2 = VC2()
    self.vc2 = vc2
    let nav = UINavigationController(rootViewController: vc2)
    self.nav = nil
    self.nav = nav
    self.view.insertSubview(nav.view, belowSubview: self.aboveButton)
    nav.view.snp.makeConstraints {
      $0.edges.equalToSuperview()
    }
    vc2.show()
  }
  
  @objc private func closeView() {
    guard self.vc2 != nil else { return }
    self.vc2?.hide { [weak self] in
      self?.vc2?.view.removeFromSuperview()
      self?.vc2 = nil
    }
  }
}

* 전체 코드: https://github.com/JK0369/ExPresentDismissAnimation

Comments