Swift: PinchZoom이 가능한 Image Viewer 만들기

Heechan
HcleeDev
Published in
10 min readMay 2, 2021
Photo by Rirri on Unsplash

앱을 만들다보니, 우리가 흔히 휴대폰의 앨범에서 사진을 볼 때처럼 확대축소(즉, PinchZoom), 드래그가 가능한 Image Viewer를 만들어야 했다. 아래 움짤을 보면 어떤 느낌인지 알 수 있을 것이다.

이는 아이폰의 기본 제공 앨범 앱인데, 여기서는 확대, 축소, 드래그 등의 기능을 지원한다. 사실 이는 Xcode 시뮬레이터로 돌린거라 멀티 터치가 중심에서 밖에 안되는데, 실제 아이폰 유저라면 본인이 원하는 곳에 확대 제스처를 취해 원하는 부분을 확대할 수 있고, 최대치로 확대하거나 최소치로 축소할 경우 살짝의 진동도 느껴진다는 것을 경험했을 것이다.

그래서, 이런 기능을 만들고자 고민했던 과정과 (아직 버그가 있지만) 나름 찾았던 해결법을 작성해보고자 한다.

SwiftUI 만으로는 할 수 없다

처음에는 위 앨범 같은 기능을 무난하게 제공해주는 SwiftUI View가 없을까 해서 찾아보았다. 하지만 안타깝게도 그런 View는 찾을 수 없었다. 있었다면 이 블로그 포스팅도 없었을텐데…

그래서 나름 대체할 수 있는 방법이 없을까 고민해보았으나, 위 움짤처럼 부드럽게 만들기는 무리가 있다는 점을 깨달았다.

첫 번째는 Magnification Gesture를 이용하려고 했으나, 아직 Magnification Gesture에는 미흡한 점이 있었다. 원하는 부분을 확대하고 싶었으나, 현재 보이는 화면의 정중앙으로만 확대가 되어 사용자가 원하는 경험을 하기 어려웠다.

그래서 두 번째로는 사용자의 손가락이 닿는 좌표를 인식하는 방법을 찾아보았다. 하지만 그런 방법은 사실상 없었고, DragGesture를 동시에 두 개 받는 방법을 찾지 못했을 뿐더러 인식할 수 있다 하더라도 Magnification과 어떻게 부드럽게 연결시킬 수 있을지는 생각해내지 못했다.

따라서 SwiftUI로는 만들 수 없다(이 세상 어딘가의 능력자 분은 만들긴 하시겠지만, 일단 저는 못만들었습니다…)고 마음 속으로 결론을 내렸다.

하지만 평상시 사용하고 있는 앱들(카톡, 슬랙 등..)은 모두 앨범과 비슷한 기능의 Image Viewer를 제공했기에, UIKit을 활용하면 분명히 방법이 있을 것이라고 생각해 타겟을 변경해 다시 찾아보았다.

UIScrollView

처음엔 UIKit에서도 어떻게 만들어지는지 모르다보니, Image viewer, Pinch zoom 등 이런저런 키워드로 검색하면서 정보를 찾았다. 그러다 여러 영상, Github, Stackoverflow를 보면서 찾은 것이 바로 UIScrollView를 이용한다는 것이었다.

사실 SwiftUI의 ScrollView는 수평, 수직 방향 중 한 뱡향으로 스크롤하는 것만 제공하지, PinchZoom이나 내부 Content를 자유로운 방향으로 스크롤하는 것은 제공하지 않았다. ScrollView도 UIScrollView를 활용해서 만든 것이지 않나? 궁금증이 들어 View를 분석해보았지만 ScrollView는 UIScrollView와 관련된 계층을 가지고 있지 않았다.

UIScrollView는 정말 View 안에 content를 넣어주면, 해당 컨텐츠를 어느 방향이든 Scroll할 수 있고 PinchZoom도 약간의 설정만 조정하면 가능하게 해주었다.

실제로 UIScrollView를 사용하는 라이브러리를 Github 어딘가에서 주워 실행해보니, 내가 원하던 Image Viewer 기능을 UIScrollView가 지원하고 있었다.

이 기능을 코드로 구현한 것을 가져와보았다. 이는 UIScrollView에 UIImage를 subview로 설정하는 코드다.

일단 코드가 이대로 돌아가진 않을 것이다. 원래 Constraint, Inset도 다 설정해주어야 하나… 핵심적인 내용을 담은 부분만 가져와보았다.

이는 UIScrollView와 UIImage를 사용하는 경우다.

  • ViewController는 UIViewController와 UIScrollViewDelegate 프로토콜을 따른다.
  • scrollView와 imageView를 정의해준다.
  • init 안에 있는 작업들은 반드시 init에서 해야 하는 것은 아니긴 하다. viewDidLoad에서 해도 된다.
  • init 에서는 super.init을 실행해주고, scrollView의 여러 프로퍼티를 설정해준다. 나의 경우에는 최대 확대 비율을 300%, 최소 축소 비율을 80%로 설정했다. bouncesZoom은 최대, 최소 값에 도달했을 때 사진이 그에 맞춰지는 효과 + 진동이 살짝 느껴지는 효과를 설정하는 것으로 알고 있다. show~ScrollIndicator는 스크롤바 노출 여부를 설정하는 프로퍼티다.
  • imageView는 scrollView의 하위 뷰로 설정해준다. 이렇게 하면 scrollView의 content가 imageView가 된다.
  • viewForZooming 메서드는 반드시 설정해주어야 한다. 여기서 확대, 축소할 content를 return해주어야 PinchZoom이 가능하다. 애플 도큐먼트에도 아래와 같이 작성되어 있다. scrollViewDidEndZooming도 설정해주어야 한다고 하는데 scrollViewDidZoom만 설정해주어도 된다. 다른 예시에서는 안해주는 경우도 많이 보아서, viewForZooming이 일단 가장 중요한 것 같다.

For zooming and panning to work, the delegate must implement both viewForZooming(in:) and scrollViewDidEndZooming(_:with:atScale:).

하지만 나는 SwiftUI를 사용하는걸

애초에 나는 UIKit이 아닌 SwiftUI 기반의 앱을 만들고 있고, 이 포스팅에선 다루지 않을 것이지만 이후 이미지 위에 그림을 그리거나 수정을 가하는 기능도 만들어야 했다. 하지만 그런 기능들은 SwiftUI로 충분히 만들 수 있고, UIKit을 잘 모르는 내 입장에서도 그게 편했다.

그래서 당장 UIViewControllerRepresentable을 이용해 SwiftUI와 UIKit을 연결시키더라도, UIImage를 이용하면 차후 원하는 기능을 만들 수가 없었다.

그러다 SwiftUI도 UIKit에다가 집어넣는 방법이 있을 것 같다는 생각이 들었고, SwiftUI로 만든 View 자체를 UIScrollView의 content로 집어넣으면 내가 고민하던 문제들이 대부분 풀릴 것이라 생각해 찾아보았다.

찾아보니 UIHostingController라는 것이 있었다. 이는 UIKit에서 SwiftUI View를 사용할 수 있게 해주는 클래스다. UIHostingController로 받아오는 View를 그대로 UIScrollView의 content로 사용할 수 있다면 그게 원하던 바이다.

이를 이용한 해결법

일단 MyScrollView라는 이름으로 UIViewControllerRepresentable을 따르는 View를 만들어 SwiftUI에서 사용할 수 있도록 했다. content에 View를 받도록 설정해두어서, 이 MyScrollView를 사용할 때는 아래 코드처럼 사용하면 된다.

MyScrollView(content: {    KFImage(URL(string: #URL#))    .resizable()    .aspectRatio(contentMode: .fit)})
  • update 메서드는 SwiftUI View, 즉 hostedView 안에서 변화가 있을 때 updateUIViewController가 실행되어야 하는 경우가 있는데, 그때 사진 위치 등을 조정하기 위해 만든 메서드다.
  • init에서는 hostedView를 만들고 약간의 설정을 가한 뒤 scrollView의 subview로 집어 넣었다.
  • alignment 함수는 hostedView의 위치를 scrollView 내의 중앙에 위치시키기 위해 constraint를 설정한 것인데, 이 부분에 오류가 있을 것이다. 현재 가지고 있는 오류는 가로가 더 긴 사진의 경우 문제 없이 중앙에 사진이 등장하나, 세로가 더 긴 사진은 가로 크기에 맞춰진다는 문제가 있다. 그리고 이를 init에서 설정하는 것이 아직 오류가 하나 있는 것 같은데 그건 아래viewDidAppear에서 다루겠다.
  • recenter 함수는 사진을 움직이는 과정에서 이상하게 위치하게 될 시 다시 중심으로 데리고 오는 함수다. init과 scrollViewDidZoom에서 불러진다. scrollViewDidZoom에서 불러지는 이유는 줌이 종료되었을 때 사진의 위치를 다시 적당히 맞춰주기 위해서다.
  • viewDidAppear같은 경우 SwiftUI에서 view가 실제로 보여질 때 호출되는 함수다. 반드시 설정해야 하는 것은 아니나, 나의 경우 Kingfisher를 이용해 온라인에서 이미지를 받아오므로 View가 초기화되는 시점과 이미지 로드가 완료되어 보이는 시점이 다르다. 그래서 사실 init 함수가 실행되는 시점에서의 hostedView, 즉 scrollView의 content의 크기는 (0, 0)이라 alignment 메서드가 실행되면서 설정되는 Constraint 등의 정보가 명확하지 않을 것으로 예상된다. 그래서 오류가 발생하는 것일 수도 있다.
  • viewDidAppear에서 alpha 값을 조정하는 이유는 이미지를 로드한 후 recenter 메서드를 통해 위치까지 한 번 더 조정한 후 alpha를 1로 만들어 이미지가 로드되는 동안은 유저들에게 그 모습을 보이지 않기 위함이다.

210530추가) 수정하면서 달라진 부분이 있으니 이 글 가장 하단에 있는 링크로 넘어가 수정된 부분을 확인하면 좋을 것 같다.

이 MyScrollView를 이용해 이미지를 아래 움짤처럼 볼 수 있도록 만들었다.

BOXBOX나 좌상단 버튼은 추가적인 기능으로, 지금 코드와는 상관없다

최소로 축소했을 때 자동으로 bounced되는 모습과, 내가 원하는 Image Viewer로서의 기능을 나름 행할 수 있게 되었다.

결론

아직 완벽하지는 않고, 의도치 않은 기능적 오류도 분명히 존재한다만… 이를 기반으로 더 많은 기능을 만들 수 있을 것으로 기대된다. 사실 SwiftUI로만 개발을 하다보니 아직 Constraint와 Inset에 대해서 잘 모르는 점도 있어 관련 공부도 조만간 해서 올릴 수도 있을 것 같다. 생각해보면 나름 iOS 개발자라는데 이걸 모르는 것도 좀… 그냥 빨리 올해 WWDC에서 SwiftUI나 대폭 발전시켜서 보여줬으면 좋겠다.

이 글에 작성한 내용은 혹시 문제를 좀 더 해결하거나 발전된 부분이 있다면 업데이트할 수 있도록 할 예정이다. 읽다가 개선할 수 있는 점이 발견될 시 댓글로 남겨주시면 반영할 수 있도록 하겠다.

210530추가) 살짝 개선한 부분이 있었다. 아래 글을 살펴보면 도움이 될 것이다! => 추가) PinchZoom이 가능한 Image Viewer 만들기 기능 수정 | by Heechan | HcleeDev | May, 2021 | Medium

--

--

Heechan
HcleeDev

Junior iOS Developer / Front Web Developer, major in Computer Science