스크롤 뷰 사용하는 방법 – UIScrollView

제한된 크기의 스크린을 가지고 있는 iOS 기기에서 고해상도의 이미지를 보여줄 때는 화면에 맞게 이미지 사이즈를 축소하거나, 화면상에 이미지의 일부만을 표시하면서 스크롤을 통해서 이미지를 탐색하게 한다. 스크롤뷰는 이러한 포토뷰어 등에서 많이 사용되며, 이를 위해 코코아 터치에서는 UIScrollView를 제공한다. UIScrollView는 간단한 코드로도 기본적인 스크롤 뷰 기능을 제공하며, 손쉽게 핀치를 통한 줌인/줌아웃을 지원할 수도 있다. 이 포스트에서는 UIScrollView를 생성하고 추가하는 기본적인 사용에서 핀치를 통한 줌인/줌아웃과 더블 탭을 통한 자동 확대를 어떻게 구현하는지 설명할 것이다.

UIScrollView

UIScrollView는 UIView의 서브클래스로 실제 화면상에 표시되는 크기(해당 뷰의 frame.size에 해당한다)보다 더 큰 가상의 서브 뷰를 가지고 있다. 따라서 실제 크기와 콘텐츠의 크기를 알려주면, 가상의 콘텐츠 영역의 시작점의 좌표를 이동시켜 화면에 표시되는 영역을 변경하여 스크롤이 일어나는 것처럼 느끼게 해주는 것이다. 가상의 콘텐츠 뷰는 직접적으로 액세스하지 않으며, 이 영역은 매우 얇기 때문에 스크롤뷰에 서브 뷰를 추가해주는 것만으로 콘텐츠를 스크롤해서 볼 수 있다.

따라서 UIScrollView의 크기와 관련된 속성은 다음과 같이 구성된다.

  • frame.size : 부모 뷰 상에서 스크롤뷰가 차지하고 있는 크기로 실제 뷰 포트의 크기이다.
  • contentSize : 실제 콘텐츠 영역의 크기이다.

따라서 스크롤뷰를 생성하기 위한 방법은 UIView와 동일하며, 대신에 ContentSize 속성값만 정확하게 지정해주면 된다.

스크롤 뷰 만드는 법

  1. 부모뷰에서 표시될 위치와 크기에 따라 frame 속성 값을 정의하고 이 값에 따라 새 인스턴스를 만든다. (init(frame:))
  2. 스크롤 뷰가 표시할 콘텐츠영역의 크기를 contentSize 속성값으로 정의한다.
  3. 콘텐츠로 사용될 뷰 (이미지 뷰나 기타 커스텀 뷰)를 스크롤뷰의 서브 뷰로 추가한다.
  4. profit!

다음은 프로젝트 번들에 포함된 큰 이미지가 있다고 가정하고 이를 부모뷰의 전체 영역의 사이즈에 가득 채워서 스크롤뷰를 통해 표시하는 코드이다.

/// in a UIViewController
// ...
var scrollView: UIScrollView!
var imageView: UIImageView!

override func viewDidLoad() {
  super.viewDidLoad()
  // ...
  let frameSize = view.bounds.size
  scrollView = UIScrollView(frame: CGRect(origin: CGPoint.zero, size: frameSize)
  /// 이미지는 번들에 포함되어 있음을 가정한다.
  let iamge = UIImage(named: "sample")
  let iamgeView = UIImageView(image: image)
  scrollView.contentSize = imageView.bounds.size
  scrollView.addSubview(imageView)
  view.addSubView(scrollView)
  ...
 }

이렇게 콘텐츠 영역의 크기만 정의해주면 스크롤뷰는 기본적인 스크롤 동작을 모두 공짜로 제공해준다. 그런데 이렇게 만든 스크롤 뷰는 뭔가 빠진 느낌이 난다. 사진 앱에서 많이 보이는 바로 그 기능, 두 손가락으로 핀치하여 줌인/줌아웃하는 기능이다. 이건 왜 안될까?

핀치를 통한 줌인/줌아웃

줌인/줌아웃을 하기 위해서는 조건이 필요하다.  스크롤 뷰를 통해서 보여지는 콘텐츠가 무한정 확대/축소 될 수는 없기 때문에,  최대최소 비율을 지정해야 한다. 그리고 중요한 또 한가지 사실은 스크롤 뷰의 콘텐츠를 확대 축소하기 위해서는 스크롤 뷰에게 어떤 뷰를 확대할 것인지를 알려주어야 한다. 즉 스크롤뷰의 콘텐츠에 속하는 가상의 뷰는 가상의 뷰이며 실제로 확대/축소 되지 않는다. 실제 콘텐츠들은 스크롤 뷰 자체의 서브 뷰(들)인 셈이다. 따라서 확대/축소가 일어나기 전에 스크롤 뷰는 자신의 델리게이트에게 어떤 뷰가 확대/축소될 것인지를 확인한다.

따라서 뷰 컨트롤러는 스크롤뷰의 델리게이트가 되어야 하고, (혹은 제 3의 객체가 이를 알려주어야 한다.) 동시에 최대/최소 확대 비율을 알려주어야 한다. 이 값들은 기본적으로 1.0으로 세팅되어 있기 때문에 이 값을 변경하지 않으면 델리게이트 프로토콜을 준수하더라도 실제 확대가 일어나지 않는다.

override func viewDidLoad() {
  super.viewDidLoad()

  // ...
  scrollView.maximumZoomScale = 4.0
  scrollView.minimumZoonScale = 0.1
  scrollView.delegate = self

  view.addSubView(scrollView)
  ...
 }

스크롤뷰의 델리게이트는 UIScrollViewDelegate 프로토콜을 따르는데 스크롤뷰에서 스크롤이 발생하거나 끝날 때, 줌이 시작되거나 끝날 때의 이벤트를 받아서 처리할 수 있는 메소드들이 정의되어 있다. 그리고 앞서 언급된 줌인/줌아웃에서 어떤 뷰가 확대축소될 것인지를 알려주는 메소드가 있다. 이 메소드를 구현하여 이미지 뷰가 확대되어야 함을 스크롤 뷰에게 알리자.

class ViewController: UIViewController, UIScrollViewDelegate {
// ...

func viewForZooming(in scrollView: UIScrollView) -> UIView? {
    return imageView
}

여기까지 작성하면 실제로 핀치를 통해서 확대/축소가 가능해짐을 알 수 있다. 그러면 더블 탭하여 확대하거나 축소하는 것은 어떻게 해야 할까?

더블 탭을 통한 줌

많은 포토 뷰어 들이 더블탭을 통해서 줌인/줌아웃을 하는 경우를 볼 수 있는데, 이 기능은 스크롤뷰에서 자체적으로 제공하는 기능이 아니다.  별도의 장치(!)를 통해서 더블탭을 감지하고, 상황에 따라서 스크롤뷰로 하여금 확대/축소를 프로그래밍적으로 처리하도록 요청해야 한다.

더블탭의 감지는  UITapGestureRecognizer를 통해서 인식할 수 있다. 여러 옵션값을 어떻게 주는가에 따라서 세번 탭해서 최대/최소로 확대/축소하게 하는 옵션을 더 할 수도 있겠다. 스크롤뷰에게 콘텐츠를 특정 비율로 확대하라는 명령은 setZoomScale(_:animated:)zoom(to:animated:)를 호출하는 것으로 실행할 수 있다.

보통 더블탭으로 확대 축소 하는 경우는 정해진 비율로 점프하는 방식으로 수행된다. 이 예제에서는 다음과 같이 처리할 생각이다.

  1. 현재 비율이 1.0 미만 (축소된 경우)인 경우에는 원본 비율로 확대한다.
  2. 1.0에서 최대 비율 사이의 값일 때는 최대 비율로 확대한다.
  3. 최대 비율에서 더블탭하면 최소 비율로 돌아간다.

현재 확대/축소 비율은 zoomScale 값을 통해 알 수 있다. 이 값이 특정 구간에 있는 지 검사하는 방식으로 어떤 동작을 수행할지를 결정할 수 있다.

정리해보면 더블 탭을 통한 줌 구현은 다음의 절차를 거친다.

  1. UITapGestureRecognizer 객체를 생성하여, 더블 탭 동작을 디스패치할 수 있게 준비한다.
  2. 실제 더블 탭 동작을 받아야 하는 객체는 스크롤 뷰이므로, 스크롤 뷰에 제스쳐 인식기를 추가한다.
  3. 제스쳐 인식기의 타깃은 뷰 컨트롤러가 되며, 조건에 맞는 탭 입력이 들어오면 정해진 메소드가 호출된다. 해당 메소드에서 현재 비율에 따른 확대/축소를 요청한다.

아래는 더블 탭 동작을 위한 구현이 추가된 소스이다.

override viewDidLoad() {
  ...
  // 제스쳐인식기를 생성하고 추가한다. 
  let doubleTap = UITapGestureRecognizer(target: self, action: #selector(self.tapToZoom))
  doubleTap.numberOfTapsRequired = 2
  doubleTap.numberOfTouchesRequired = 1
  scrollView.addGestureRecognizer(doubleTap)
  ...
}

func tapToZoom(_ gestureRecognizer: UIGestureRecognizer) {
  // 더블탭이 인식되면 호출된다. 
  // 현재 줌 비율에 따라서
  switch scrollView.zoomScale {
  case (scrollView.minimumZoomScale..<1.0): // 축소된 상태인 경우
    scrollView.setZoomScale(1.0, animated: true)
  case (1.0..<scrollView.maximumZoomScale): // 확대된 상태인 경우
    scrollView.setZoomScale(1.0, animated: true)
  default: // 최대 크기인 경우
    scrollView.setZoomScale(scrollView.maximumZoomScale, animated: true)
  }
}

이상의 내용으로 스크롤 뷰의 기본적인 사용법을 알아보았다. 개발자 문서를 확인해보면 보다 많은 메소드들과 옵션을 확인할 수 있으며, 그외 기능을 사용하여 특정 위치로 스크롤을 애니메이트하거나 줌/인아웃 동작이 수행된 후에 특정한 다른 동작을 수행하는 등의 처리를 하는 방법도 알아보자.

파고 들기

어떤 앱들은 처음 실행하면 앱의 기본적인 사용법이나 기능 들은 페이지 단위로 나눠서 표시해주는 기능을 제공하기도 한다. 이렇게 각 페이지를 넘겨서 이미지를 보여주는 기법 역시 스크롤 뷰를 사용한다. 시간이 되면 이런 기법에 대해서도 찾아보도록 하자.

깊게 파고 들기

기본적으로 스크롤 뷰는 콘텐츠 전체를 메모리에 올려서 필요한 부분만 보여준다. 만약 콘텐츠가 엄청나게 커다란 이미지라면 이를 어떤 식으로 스크롤뷰를 통해 (혹은 다른 어떤 방법으로) 표시할 수 있을까?

  • Pingback: [iOS] UIScrollView 사용법 – Wireframe()

  • Xcode 에뮬레이터 상에서 두손가락으로 확대모션을 취하면 viewForZooming 호출 까지 되는데, 이미지는 확대가 되지 않는데 무엇이 문제일까요? 코드는 똑같이 복사했는데말입니다…!

    • 줌 스케일 설정하는 부분과 델리게이트, 델리게이트 메소드 부분을 확인하세요