스크롤 뷰 사용하는 방법 - 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)
  }
}

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

파고 들기

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

깊게 파고 들기

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

Read more

워드프레스에서 고스트로 이전

워드프레스에서 고스트로 이전

이 글을 쓰면서도 믿기 힘든 사실인데, 블로그라는 걸 처음 시작한지가 20년이 되었습니다. 이글루스에서 처음 시작했다가, SK컴즈가 인수한다고 발표함과 동시에 워드프레스로 플랫폼을 옮겼죠. 워드프레스오 옮긴 이후에는 호스팅 환경을 이리 저리 옮기긴 했지만 거의 18년 가까이 워드프레스를 사용해온 것 같습니다. 그 동안 워드프레스는 블로깅 툴에서 명실상부한 범용CMS로 발전했습니다. 사실 웬만한 홈페이지들은 이제

By sooop
띄어쓰기에 대한 생각

띄어쓰기에 대한 생각

업무 메일을 쓸 때 가장 많이 쓰는 말 중에 하나가 메일 말미에 ‘업무에 참고 부탁 드립니다.‘인데요, 어느 날부터 아웃룩에서 이 ‘부탁 드립니다’가 틀렸다고 맞춤법 지적을 하기 시작했습니다. 맞는 말은 ‘부탁드립니다’라고 붙여 쓰는 거라고. 사실 아래아한글 시절부터 이전의 MS워드까지, 워드프로세서들의 한국어 맞춤법 검사 실력은 거의 있으나 마나 한

By sooop

구글 포토에서 아이클라우드로 탈출한 후기

한 때 구글 포토가 백업 용량을 무제한으로 제공해 주겠다고해서, 구글 포토를 사용해서 사진을 백업해왔습니다. 물론 이 이야기의 결말은 저나 이 글을 읽고 있는 여러분이나 모두 알고 있습니다. 사실 AI에게 학습 시킬 이미지 데이터를 모으기 위한 것일 뿐이라거나 하는 이야기는 그 당시에도 있었습니다만, 에이 그래도 구글인데 용량은 넉넉하게 주겠지…하는 순진한

By sooop

Julia의 함수 사용팁

연산자의 함수적 표기 Julia의 연산자는 기본적으로 함수이며, 함수 호출 표기와 같은 방식으로 호출하는 것이 가능합니다. 또한 그 자체로 함수이기 때문에 filter(), map() 과 같이 함수를 인자로 받는 함수에도 연산자를 그대로 적용하는 것이 가능합니다. 특히 + 연산자는 sum() 함수와 같이 여러 인자를 받아 인자들의 합을 구할 수 있습니다. 2 + 3 # = 5 +(2,

By sooop