본문 바로가기

IOS

SwiftUI - 이미지 줌 기능을 달아보자

요즘 커머스 관련한 앱을 보다보면, 이미지를 확대해서 보는 기능이 있다 

SwiftUI 상에 있는 MagnificationGesture 를 이용해 구현 했을려 했으나, 필자가 원하는 방식대로 적용하기 어려워 

ViewController 의 힘을 빌렸다. ViewPager 상에서 Zoom 을 이용해 확대 기능 + 더블탭을 이용해 다시 원상태로 되돌릴수 있도록 만들었다.


1. ImageZoomRepresentable

import Foundation
import SwiftUI
import Combine

struct ImageZoomRepresentable<Content: View>: UIViewControllerRepresentable {
    
    var content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    func makeUIViewController(context: Context) -> ImageZoomViewController {
        let vc = ImageZoomViewController()
        vc.hostingController.rootView = AnyView(self.content())
        return vc
    }

    func updateUIViewController(_ viewController: ImageZoomViewController, context: Context) { }
}


class ImageZoomViewController: UIViewController {
    
    lazy var scrollView: UIScrollView = {
        let v = UIScrollView()
        v.isPagingEnabled = true
        return v
    }()
    
    var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))

    var zoomScale: CGFloat = 0 {
        didSet {
            if zoomScale > 1.0 {
                scrollView.isScrollEnabled = true
            } else {
                scrollView.isScrollEnabled = false
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.scrollView)
        self.parentConstraint(of: self.scrollView, to: self.view) // scrollview -> view

        self.hostingController.willMove(toParent: self)
        self.scrollView.addSubview(self.hostingController.view)
        self.childConstraint(of: self.hostingController.view, to: self.scrollView, isZoom: false) // image -> scrollview
        self.hostingController.didMove(toParent: self)
        
        scrollView.maximumZoomScale = 2.5
        scrollView.minimumZoomScale = 1
        
        scrollView.showsVerticalScrollIndicator = false
        scrollView.showsHorizontalScrollIndicator = false
        
        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(onDoubleTap(gestureRecognizer:)))
        tapRecognizer.numberOfTapsRequired = 2
        
        scrollView.addGestureRecognizer(tapRecognizer)
        
        scrollView.delegate = self
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { [self] in
            initScrollContent()
        }
    }
    
    //MARK: - View Constraint
    //가장 부모 뷰와 스크롤뷰에 대한 위치 제약조건 (ScrollView)
    func parentConstraint(of viewA: UIView, to viewB: UIView) {
        viewA.translatesAutoresizingMaskIntoConstraints = false
        viewB.addConstraints([
            viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
            viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
            viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
            viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor)
        ])
    }
    
    //스크롤뷰와 자식 뷰에 대한 위치 제약조건 (Content (Image))
    func childConstraint(of viewA: UIView, to viewB: UIView, isZoom: Bool) {
        viewA.translatesAutoresizingMaskIntoConstraints = false

        viewB.addConstraints([
            viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
            viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
            viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
            viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
            viewA.heightAnchor.constraint(equalToConstant: 270)
        ])
    }
    
    // 더블 클릭했을때 이미지 원복
    @objc func onDoubleTap(gestureRecognizer: UITapGestureRecognizer) {
        initScrollContent()
    }
    
    // init 시에 부모 뷰와 scrollview content 를 정렬
    func initScrollContent() {
        scrollView.setZoomScale(1.0, animated: true)
        zoomScale = 1.0
        
        let offsetX = max((self.view.bounds.size.width - self.hostingController.view.frame.width) * 0.5, 0.0)
        let offsetY = max((self.view.bounds.size.height - self.hostingController.view.frame.height) * 0.5, 0.0)
        scrollView.contentInset = UIEdgeInsets(top: offsetY, left: offsetX, bottom: offsetY, right: offsetX);
    }
}

extension ImageZoomViewController: UIScrollViewDelegate {
    //MARK: - ScrollView Zoom On
    // 줌 하고자 하는 뷰를 리턴한다.
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return self.hostingController.view
    }
    
    //MARK: - ScrollView Zoom 최대치, 최소치 컨트롤
    // 스크롤 할때마다 계속 호출하여 현재 줌 스케일 값을 할당한다.
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        zoomScale = scrollView.zoomScale
    }
    
    //MARK: - ScrollView Zoom End
    // scrollview Zoom 완료시에 scrollview content를 중간으로 정렬함
    func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
        let offsetX = max((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0)
        let offsetY = max((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0)

        scrollView.contentInset = UIEdgeInsets(top: offsetY, left: offsetX, bottom: offsetY, right: offsetX);
    }
}

상세한 설명은 주석처리 했으나, 간략하게 정리하자면

실제 줌을 하여도 스크롤을 이용해서 내용을 확인 해야하기 때문에, 바깥은 ScrollView 로 구성하고, 확대 하고자 하는 뷰를 추가한다.

줌관련한 옵션을 추가하고 Delegate 를 이용하여 해당 함수 내용에 맞게 조정한다.


2. ImageZoomRepresentable

VStack {
    ImageZoomRepresentable {
        Image("image")
            .resizable()
            .frame(width: UIScreen.main.bounds.width ,height: 270)
    }
}

적용은 정말 간단하다.


3.  적용

시뮬레이터라 그런지 버벅..


마치며

MagnificationGesture 를 이용해 완벽히 구현하진 못했지만, 많은 블로그를 참고해 만들어 결과가 나와서 다행이다.

ViewPager 를 이용해 다중 이미지를 보여주거나, 현재 이미지 인덱스를 보여줄수 있는 뷰를 추가하며 더욱더 완성도를 높힐수 있을것으로 보인다.

참고

 

 

https://stackoverflow.com/questions/9008975/how-to-tap-to-zoom-and-double-tap-to-zoom-out-in-ios

 

How to tap to zoom and double tap to zoom out in iOS?

I'm developing an application to display a gallery of UIImages by using a UIScrollView, my question is, how to tap to zoom and double tap to zoom out, how does it work when handling with UIScrollView.

stackoverflow.com

         https://gist.github.com/timothycosta/0d8f64afeca0b6cc29665d87de0d94d2

 

UIScrollView wrapped for SwiftUI

UIScrollView wrapped for SwiftUI. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

         https://github.com/1992Shubham/ImageViewer/blob/master/ImageViewer/ViewController.swift

 

GitHub - 1992Shubham/ImageViewer

Contribute to 1992Shubham/ImageViewer development by creating an account on GitHub.

github.com

         https://stackoverflow.com/questions/1062693/how-to-disable-horizontal-scrolling-of-uiscrollview

 

How to disable horizontal scrolling of UIScrollView?

I have a UIView like iPhone's Springboard. I have created it using a UIScrollView and UIButtons. I want to disable horizontal scrolling on said scrollview. I want only vertical scrolling. How do I

stackoverflow.com

         https://stackoverflow.com/questions/1316451/center-content-of-uiscrollview-when-smaller/31421691#31421691

 

Center content of UIScrollView when smaller

I have a UIImageView inside a UIScrollView which I use for zooming and scrolling. If the image / content of the scroll view is bigger than the scroll view, everything works fine. However, when the ...

stackoverflow.com