Swift: Drawing 기능 만들기

Heechan
HcleeDev
Published in
9 min readAug 7, 2021
Photo by Ethan Wong on Unsplash

최근에 회사에서 맡은 일 중 터치로 Drawing을 할 수 있는 기능을 구현해야 할 일이 생겼다. 기존 앱에 Drawing, Brush 느낌의 기능이 아예 없었기 때문에 처음부터 구현해야 했어서 고생을 꽤 했다. 여러 기능들로 파생할 수 있는, 가장 기본이 되는 기능인 Drawing 기능을 어떻게 구현했는지 이번주에 소개해보고자 한다.

SwiftUI를 사용하고 있기 때문에, UIViewControllerRepresentable을 통해 UIScrollView, UIImageView를 적극 활용하였다.

UIViewControllerRepresentable과 기본 ViewController 구성

사실 UIKit을 사용하지 않고 SwiftUI를 활용해서도 드로잉 기능을 만들 수 있을 것 같긴 하다. 하지만 UIViewControllerRepresentable를 사용한 이유는 이미지 확대 축소를 자연스럽게 쓸 수 있도록 하기 위함이다. 이전에 UIScrollView와 UIHostingController를 사용해 Pinch Zoom을 가능하게 하는 방법에 대해 포스팅한 적이 있는데, 그때와 비슷하지만 이번엔 UIHostingController 대신 UIImageView를 사용하려고 한다.

일단 내가 준비한 whiteImage 위에서 그림을 그려보고자 한다.

  • UIViewControllerRepresentable을 사용했고, 그 속에 ViewController를 만들어줬다. 이름은 DrawingViewController 로 UIScrollViewDelegate를 채택하고 있다.
  • DrawingViewControllerscrollViewimageView 를 가지고 있다. 이 두 가지는 모두 init 에서 초기화된다.
  • init 에서는 scrollView 에 대한 기본적인 설정들을 해준다.
  • viewDidAppear 에서는 이제 scrollViewimageView 를 편입시킨다. viewDidAppear 정도에서는 이미지 로딩이 되었다고 생각할 수 있기 때문에, 여기서 이미지 사이즈와 관련된 작업을 하는 것이 안전하긴 하다. 그래서 여기선 이미지가 짤리지 않도록 이미지의 사이즈와 scrollView 의 사이즈를 비교해 scale 을 설정한다. 이 scale 값을 이용해 scrollView 의 확대/축소 정도를 설정하고, 이미지가 짤리지 않도록 했다.
  • viewForZooming 함수는 UIScrollViewDelegate에서 제공하는 함수로, UIScrollView에서 어떤 UIView를 확대/축소할 수 있게 할 것인지 설정하는 함수다. 해당 함수를 만들지 않으면 UIScrollView에서는 Zooming이 되지 않는다.

이렇게 만들고 앱을 켜보면 하얀 이미지가 하나 검은 배경 위에 등장했을 것이다.

검은색 배경의 scrollView와 하얀색 이미지

UIPanGestureRecognizer

이번에는 UIPanGestureRecognizer 를 활용해 유저의 드래그 제스처를 담고, 그에 맞춰 반응할 수 있도록 설계해보고자 한다. 일단 위에서 봤던 ViewController의 init 에 UIPanGestureRecognizer를 새롭게 추가한다.

let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureHandler(_:)))
scrollView.addGestureRecognizer(panGestureRecognizer)

panGestureRecognizer 를 이렇게 설정할 수 있다. 위처럼 코드를 작성하면 self (= DrawingViewController )에 Pan 제스처가 들어오면 panGestureHandler 라는 함수에서 그 제스처를 처리해준다.

지금까지 SwiftUI만 써온지라 #selector를 사용하는 것은 신박한 느낌이었는데, Obj-C를 사용할 때의 흔적이 남아있는 것이었다. 그래서 여기에 들어가는, Selector 함수 앞에는 @objc를 붙여줘야 한다.

그러면 이번에는 이 panGestureHandler 를 만들어보자.

DrawingViewController 안에 위처럼 코드를 작성해준다.

일단 드래그가 인식되는 틱마다 이 함수가 실행될텐데, 이 드래그 제스처가 변화되고 있는 상황이면 이전 틱의 위치와, 현재 틱에서 인식한 위치 사이에 선을 그어버리도록 할 것이다.

따라서 lastPoint 라는 변수를 만들어주고, 상황에 맞게 lastPoint 와 현재 currentPoint 를 활용해 라인을 그려주도록 한다.

Bitmap 이미지 처리

UIKit에서는 이미지 비트맵 처리를 위해서 CGContext를 사용할 수 있도록 다양한 메서드를 제공한다. 이 CGContext는 2차원 이미지를 그릴 수 있는 지정된 크기의 ‘context’를 열어주고, 그 위에 뭔가 draw된 후 다시 이미지로 저장할 수 있도록 도와준다.

DrawingViewController 안에 새로운 drawLineFrom 함수를 만들어줬다. 사실 이 코드는 구글링하다보면 쉽게 찾을 수 있는 코드고 특히 흠잡을 곳은 없는 것 같아서 거의 그대로 사용했다.

이 함수를 작성하면서 가장 신경을 쓴 부분은 다름아닌 이미지의 ‘크기’였다. Context가 열리는 UIGraphicsBeginImageContextWithOptions 에서 주입하는 사이즈와, imageView.image?.draw 에서 주입하는 사이즈에 유의해야 한다. UIGraphicsBeginImageContextWithOptions 에 주입하는 사이즈가 바로 Context(=그림판)의 크기로, 16번째 줄에서 만들어지는 새로운 이미지는 여기서 넣은 사이즈로 정해진다. imageView.image?.draw 에서는 기존 이미지에서 어떤 부분을 이 Context에 배경으로 그려넣을지 정하는 것으로, Context의 크기를 넘어가면 짤릴 수 있다.

이 코드에서는 Context의 크기를 이미지의 사이즈와 같도록 해 이미지의 화질 저하를 방지하고, 변형 또한 되지 않도록 처리했다.

이렇게 작성하고 한번 앱을 실행해보자.

누르는걸 감지하는 포인터는 캡처 프로그램 기능이라 따로 만들어준 것 아니다…

흰색 이미지 위에 선도 그려지고 확대 축소도 되는 모습이다.

지우개 기능 만들기…와 그 이상

그린 것을 다시 지우는 지우개 기능도 한번 알아보도록 하자. 간단히 생각하면 drawLineFrom 함수의 .setBlendMode 를 변경해주면 된다.

context?.setBlendMode(.normal)
======>
context?.setBlendMode(.clear)

그런데 이렇게 하면 지금까지 우리가 Drawing을 만드는 방식에서는 함정이 한가지 있다. scrollView.backgroundColor = .blue 로 설정하고 한번 clear 모드로 실행시켜보자.

그냥 그 자리에 있는 이미지를 아예 지워버린다. 비트맵 Context 상에 이미지(하얀 이미지)를 그리고, 우리가 드래그한 곳을 .clear 하도록 했기 때문에 아예 뒷 배경이 보일 정도로 투명하게 만들어버린다.

이렇게 되면 우리가 방식을 바꿔야 한다.

Drawing한 것이 저장되는 이미지와, 배경 이미지가 저장되는 곳을 달리하고, 보여줄 때는 둘을 합쳐주는 방식으로 보여주면 가능하다. 이렇게 하면 지우개 기능도 Drawing한 것이 저장된 이미지에만 적용하면 된다.

그러면 drawLineFrom 과 다른 코드들을 이렇게 한번 고치고 추가해보자.

init 에서도 originalImage = UIImage(named: "whiteImage")) 이런 식으로 originalImage 를 초기화시켜주면 된다.

위 코드에서 drawLineFrom 에서의 이미지는 drawingImage 라는 곳에 그려지고, 저장됨을 확인할 수 있다. 그리고 실제로 앱에서 보이게 되는 imageView 의 이미지는 merge 된 이미지를 보여주도록 한다.

merge 함수에서는 originalImagedrawingImage 를 합쳐서 하나의 UIImage로 만들어준다. 이 과정은 기존의 두 이미지를 딱히 변형시키지 않고 보여주기만을 위한 계산이다. 물론 이렇게 이미지를 처리하는건 살짝 성능 이슈가 생길 수 있는 문제긴 하다.

여튼 상태 변수를 이용해 적당히 그리기와 지우기 모드를 선택할 수 있도록 개발해보고 앱을 켜보자.

우리가 의도했던 것처럼 잘 그려지고 잘 지워지는 모습이다.

결론

구글링하면서 봤던 이런저런 Drawing 코드를 긁어모아 그럴듯한 Drawing 기능을 만들어냈다. SwiftUI에다가 이식하는 작업은 직접 했는데, 좀 익숙치 못해서 쉽지 않았다. Drawing 성능의 경우 꾸준히 고민하고 있지만, 아무튼 이미지 관련 처리가 상당한 작업이라는 사실은 새삼 깨달았다. 학부 수업 들을 때도 느낀거긴 했는데… 요즘 이미지들 화질이 너무 좋긴 하다.

사실 애플 펜슬이 나오면서 함께 나온 PencilKit이라는 것에서 Canvas 기능을 지원하긴 하던데, 일단 이런 방식으로 만들어본 것이라 PK에 대해서도 차후 알아보면 좋을 것 같다.

비슷하게 다음주에는 이미지 메타데이터와 관련해 비교적 가벼운 개념 내용을 다뤄볼까 생각하고 있다.

참고한 것

How To Make A Simple Drawing App with UIKit and Swift | raywenderlich.com

ios — How to merge two UIImages? — Stack Overflow

--

--

Heechan
HcleeDev

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