[Swift/UIKit] Scribble Sticker 손글씨 스티커 구현

양재현·2026년 2월 15일

이번 글에서는 아이패드에서 아이펜슬로 손글씨를 쓸때 Scribble로 텍스트로 바로 변환시키는 예제를 구현해보겠다.

과정은 아래와 같이 진행된다.

1. 빈뷰에 손글씨 작성
2. Scribble로 해당 위치에 텍스트필드 생성
3. 텍스트필드를 텍스트로 변경

바로 코드로 진행하겠다.

StickerTextField

class StickerTextField: UITextField {
    
    var fontSize: CGFloat = 28.0  // 기본 글씨 크기 설정
    let identifier = UUID() // 여러 스티커를 구분하기 위한 id
    
    // 코드로 UI를 만들 때 필수로 구현해야 하는 부분 (스토리보드 사용 안 하니까 에러 처리)
    required init?(coder: NSCoder) {
        fatalError("Not implemented")
    }
    
    // 텍스트 필드가 처음 만들어질 때 초기 설정
    override init(frame: CGRect) {
        super.init(frame: frame)
        text = "" // 처음엔 빈칸
        borderStyle = .roundedRect // 둥근 테두리 모양
        backgroundColor = .clear // 배경은 투명하게
        updateFont() // 폰트 설정 적용
        updateSize() // 크기 설정 적용
    }
    
    // 위치(origin)만 알려주면 기본 크기(12x20)로 만들어주는 편리한 초기화 방법
    convenience init(origin: CGPoint) {
        self.init(frame: CGRect(origin: origin, size: CGSize(width: 12, height: 20)))
    }
    
    // 폰트를 설정하고, 글씨 크기에 맞게 텍스트 필드 크기도 다시 맞추는 함수
    func updateFont() {
        font = UIFont(name: "Futura-Bold", size: fontSize)
        updateSize(centerResize: true)
    }
    
    // 글자가 써질 때마다 텍스트 필드의 크기를 자동으로 조절해주는 함수
    func updateSize(centerResize: Bool = false) {
        let oldSize = frame.size
        // 폰트 크기를 기준으로 얼만큼의 공간이 필요한지 계산해요.
        let size = sizeThatFits(CGSize(width: 1024, height: fontSize))
        let oldOrigin = frame.origin
        
        let deltaX = size.width - oldSize.width
        let deltaY = size.height - oldSize.height
        
        // centerResize가 true면 중앙을 기준으로 크기를 늘리고, 아니면 원래 위치를 유지해요.
        let origin = centerResize ? CGPoint(x: oldOrigin.x - deltaX / 2, y: oldOrigin.y - deltaY / 2) : oldOrigin
        frame = CGRect(origin: origin, size: size)
    }
}
  • StickerTextField, 말 그대로 스티커처럼 붙일 텍스트 필드다.
  • 나중에 빈뷰에서 아무곳에나 손글씨를 쓰면 이 스티커 텍스트 필드가 만들어질 것이다.

ViewController

class ViewController: UIViewController {
    var stickerTextFields: [StickerTextField] = [] // 스티커(텍스트 필드)들을 모아두는 배열
    var stickerContainerView = UIView() // 스티커들을 올려둘 커다란 투명 판(뷰)
    let rootViewElementID = UUID() // 투명 판(뷰)을 가리키는 고유 ID
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 투명 판(뷰)을 화면 전체 크기로 맞추고 화면에 추가합니다.
        stickerContainerView.frame = view.bounds
        stickerContainerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(stickerContainerView)
        
        // 손글씨(Scribble) 딜리게이트 부여
        let indirectScribbleInteraction = UIIndirectScribbleInteraction(delegate: self)
        stickerContainerView.addInteraction(indirectScribbleInteraction)
        
    }
    
    // 텍스트 필드에 글자가 바뀔 때마다 실행되는 함수
    @objc func handleTextFieldDidChange(_ textField: UITextField) {
        guard let stickerField = textField as? StickerTextField else { return }
            stickerField.updateSize() // 글자 바뀔때마다 텍스트필드 크기 바꿔줌
    }
    
    // 텍스트 필드를 텍스트로 바꿔줌
    func transformToLabel(_ stickerField: StickerTextField) {
        guard let text = stickerField.text else { return }
        
        let label = UILabel()
            label.text = text
            label.font = stickerField.font
            label.textColor = .black // 원하는 글자 색상
            label.sizeToFit() // 글자 크기에 딱 맞게 조절
            label.center = stickerField.center // 텍스트 필드가 있던 위치 그대로
        
        // 화면에 텍스트를 추가하고, 기존의 텍스트 필드(스티커)는 지워버립니다.
        stickerContainerView.addSubview(label)
        stickerField.removeFromSuperview()
    }
    
    // 특정 위치에 새로운 스티커(텍스트 필드)를 만들어서 추가하는 함수
    func addStickerFieldAtLocation(_ location: CGPoint) -> StickerTextField {
        let stickerField = StickerTextField(origin: location)
        stickerField.delegate = self
        // 글자가 바뀔 때마다 감지하도록 설정
        stickerField.addTarget(self, action: #selector(handleTextFieldDidChange(_:)), for: .editingChanged)
        
        stickerTextFields.append(stickerField) // 배열에 저장
        stickerContainerView.addSubview(stickerField) // 화면에 표시
        return stickerField
    }
    
}
  • 메인 화면이 될 ViewController다.
  • 스티커 텍스트 필드가 붙여질 화이트 보드라고 생각하면되는데, 메서드를 하나씩 보자면
  • objc func handleTextFieldDidChange(_ textField: UITextField) 이 친구는 글자가 늘어날수록 텍스트 필드도 같이 커지게 해주는 메서드다.
  • func transformToLabel(_ stickerField: StickerTextField) 이 친구는 보드판에 붙인 스티커 텍스트 필드를 일반 텍스트로 바꿔주는 역할을 한다.
  • func addStickerFieldAtLocation(_ location: CGPoint) 이 친구는 보드판 아무데서나 새로운 스티커 텍스트 필드를 추가할 수 있게 해주는 메서드다.

UIIndirectScribbleInteractionDelegate(간접)

extension ViewController: UIIndirectScribbleInteractionDelegate {
    
    // 손글씨를 쓸 수 있는 대상이 뭐뭐가 있는지
    func indirectScribbleInteraction(_ interaction: UIInteraction, requestElementsIn rect: CGRect, completion: @escaping ([ElementIdentifier]) -> Void) {
            completion([rootViewElementID])
        }
    
    // Scribble이 활성화되는 구역
    func indirectScribbleInteraction(_ interaction: UIInteraction, frameForElement elementIdentifier: UUID) -> CGRect {
        return stickerContainerView.frame
    }
    
    // 이 ID를 가진 필드가 이미 포커스 상태인지
    func indirectScribbleInteraction(_ interaction: UIInteraction, isElementFocused elementIdentifier: UUID) -> Bool {
       return false
    }
    
    // 펜슬이 닿는 순간 그 위치(focusReferencePoint)에 해당하는 텍스트 필드 생성
    func indirectScribbleInteraction(_ interaction: UIInteraction, focusElementIfNeeded elementIdentifier: UUID, referencePoint focusReferencePoint: CGPoint, completion: @escaping ((UIResponder & UITextInput)?) -> Void) {
            let stickerField = addStickerFieldAtLocation(focusReferencePoint) // 펜슬 닿은 그 위치에 새 스티커를 만들기
            stickerField.becomeFirstResponder() // 포커스 활성화
            completion(stickerField) // 텍스트 필드에 방금 쓴 글씨 입력
    }
    
    // 빈 배경에 글씨 쓸 때 포커스 조금 뜸들이기 (false시 텍스트 필드 커서가 깜빡이다 사라짐)
    func indirectScribbleInteraction(_ interaction: UIInteraction, shouldDelayFocusForElement elementIdentifier: UUID) -> Bool {
        return true
    }
    
    // 손글씨가 끝났을때
    func indirectScribbleInteraction(_ interaction: UIInteraction, didFinishWritingInElement elementIdentifier: UUID) {
        view.endEditing(true) // 이 뷰 안에 있는 모든 입력창(키보드)을 한 번에 다 닫기
    }
}
  • extension으로 UIIndirectScribbleInteractionDelegate를 받아준다.

  • 각각의 메서드는 보드판에서 애플펜슬로 Scribble 상호작용에 관한 모든 내용을 담고 있다.

  • 손글씨를 쓸 수 있는 대상부터 활성화되는 구역, 포커스 상태 여부, 펜슬이 닿는 위치, 포커스 지연, 손글씨 작성 완료 까지 delegate를 수행한다.

UITextFieldDelegate

extension ViewController: UITextFieldDelegate {
    // 글씨 입력이 완전히 끝났을 때 자동으로 불리는 함수
    func textFieldDidEndEditing(_ textField: UITextField) {
        guard let stickerField = textField as? StickerTextField else { return } //StickerTextField로 만든 텍스트 필드 맞는지 확인
        
        transformToLabel(stickerField) //텍스트로 변환
    }
}
  • UITextField의 대리자다.
  • 하나의 메서드만 있는데 func textFieldDidEndEditing(_ textField: UITextField) 는 손글씨가 끝나고 텍스트 필드가 닫혔을때 자동으로 불리게된다. 그 후 텍스트 필드에 써져있는 글자를 텍스트로 변환해주고 있다.

전체코드

import UIKit

// MARK: - 1. 스티커 역할을 하는 텍스트 필드 (StickerTextField)
/*
 스티커TextField는 화면에 글씨를 쓰면 나타나는 텍스트 입력창입니다.
 내용이 길어지면 자동으로 크기가 늘어나고, 애플 펜슬로 대충 근처에 써도(Scribble)
 인식이 잘 되도록 인식 범위를 넓혀놓은 똑똑한 텍스트 필드예요.
 */
class StickerTextField: UITextField {
    
    var fontSize: CGFloat = 28.0  // 기본 글씨 크기 설정
    let identifier = UUID() // 여러 스티커를 구분하기 위한 id
    
    // 코드로 UI를 만들 때 필수로 구현해야 하는 부분 (스토리보드 사용 안 하니까 에러 처리)
    required init?(coder: NSCoder) {
        fatalError("Not implemented")
    }
    
    // 텍스트 필드가 처음 만들어질 때 초기 설정
    override init(frame: CGRect) {
        super.init(frame: frame)
        text = "" // 처음엔 빈칸
        borderStyle = .roundedRect // 둥근 테두리 모양
        backgroundColor = .clear // 배경은 투명하게
        updateFont() // 폰트 설정 적용
        updateSize() // 크기 설정 적용
    }
    
    // 위치(origin)만 알려주면 기본 크기(12x20)로 만들어주는 편리한 초기화 방법
    convenience init(origin: CGPoint) {
        self.init(frame: CGRect(origin: origin, size: CGSize(width: 12, height: 20)))
    }
    
    // 폰트를 설정하고, 글씨 크기에 맞게 텍스트 필드 크기도 다시 맞추는 함수
    func updateFont() {
        font = UIFont(name: "Futura-Bold", size: fontSize)
        updateSize(centerResize: true)
    }
    
    // 글자가 써질 때마다 텍스트 필드의 크기를 자동으로 조절해주는 함수
    func updateSize(centerResize: Bool = false) {
        let oldSize = frame.size
        // 폰트 크기를 기준으로 얼만큼의 공간이 필요한지 계산해요.
        let size = sizeThatFits(CGSize(width: 1024, height: fontSize))
        let oldOrigin = frame.origin
        
        let deltaX = size.width - oldSize.width
        let deltaY = size.height - oldSize.height
        
        // centerResize가 true면 중앙을 기준으로 크기를 늘리고, 아니면 원래 위치를 유지해요.
        let origin = centerResize ? CGPoint(x: oldOrigin.x - deltaX / 2, y: oldOrigin.y - deltaY / 2) : oldOrigin
        frame = CGRect(origin: origin, size: size)
    }
}

// MARK: - 2. 메인 화면 (ViewController)
class ViewController: UIViewController {
    var stickerTextFields: [StickerTextField] = [] // 스티커(텍스트 필드)들을 모아두는 배열
    var stickerContainerView = UIView() // 스티커들을 올려둘 커다란 투명 판(뷰)
    let rootViewElementID = UUID() // 투명 판(뷰)을 가리키는 고유 ID
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 투명 판(뷰)을 화면 전체 크기로 맞추고 화면에 추가합니다.
        stickerContainerView.frame = view.bounds
        stickerContainerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(stickerContainerView)
        
        // 손글씨(Scribble) 딜리게이트 부여
        let indirectScribbleInteraction = UIIndirectScribbleInteraction(delegate: self)
        stickerContainerView.addInteraction(indirectScribbleInteraction)
        
    }
    
    // 텍스트 필드에 글자가 바뀔 때마다 실행되는 함수
    @objc func handleTextFieldDidChange(_ textField: UITextField) {
        guard let stickerField = textField as? StickerTextField else { return }
            stickerField.updateSize() // 글자 바뀔때마다 텍스트필드 크기 바꿔줌
    }
    
    // 텍스트 필드를 텍스트로 바꿔줌
    func transformToLabel(_ stickerField: StickerTextField) {
        guard let text = stickerField.text else { return }
        
        let label = UILabel()
            label.text = text
            label.font = stickerField.font
            label.textColor = .black // 원하는 글자 색상
            label.sizeToFit() // 글자 크기에 딱 맞게 조절
            label.center = stickerField.center // 텍스트 필드가 있던 위치 그대로
        
        // 화면에 텍스트를 추가하고, 기존의 텍스트 필드(스티커)는 지워버립니다.
        stickerContainerView.addSubview(label)
        stickerField.removeFromSuperview()
    }
    
    // 특정 위치에 새로운 스티커(텍스트 필드)를 만들어서 추가하는 함수
    func addStickerFieldAtLocation(_ location: CGPoint) -> StickerTextField {
        let stickerField = StickerTextField(origin: location)
        stickerField.delegate = self
        // 글자가 바뀔 때마다 감지하도록 설정
        stickerField.addTarget(self, action: #selector(handleTextFieldDidChange(_:)), for: .editingChanged)
        
        stickerTextFields.append(stickerField) // 배열에 저장
        stickerContainerView.addSubview(stickerField) // 화면에 표시
        return stickerField
    }
    
}


// MARK: - UIIndirectScribbleInteractionDelegate(간접) 델리게이트
extension ViewController: UIIndirectScribbleInteractionDelegate {
    
    // 손글씨를 쓸 수 있는 대상이 뭐뭐가 있는지
    func indirectScribbleInteraction(_ interaction: UIInteraction, requestElementsIn rect: CGRect, completion: @escaping ([ElementIdentifier]) -> Void) {
            completion([rootViewElementID])
        }
    
    // Scribble이 활성화되는 구역
    func indirectScribbleInteraction(_ interaction: UIInteraction, frameForElement elementIdentifier: UUID) -> CGRect {
        return stickerContainerView.frame
    }
    
    // 이 ID를 가진 필드가 이미 포커스 상태인지
    func indirectScribbleInteraction(_ interaction: UIInteraction, isElementFocused elementIdentifier: UUID) -> Bool {
       return false
    }
    
    // 펜슬이 닿는 순간 그 위치(focusReferencePoint)에 해당하는 텍스트 필드 생성
    func indirectScribbleInteraction(_ interaction: UIInteraction, focusElementIfNeeded elementIdentifier: UUID, referencePoint focusReferencePoint: CGPoint, completion: @escaping ((UIResponder & UITextInput)?) -> Void) {
            let stickerField = addStickerFieldAtLocation(focusReferencePoint) // 펜슬 닿은 그 위치에 새 스티커를 만들기
            stickerField.becomeFirstResponder() // 포커스 활성화
            completion(stickerField) // 텍스트 필드에 방금 쓴 글씨 입력
    }
    
    // 빈 배경에 글씨 쓸 때 포커스 조금 뜸들이기 (false시 텍스트 필드 커서가 깜빡이다 사라짐)
    func indirectScribbleInteraction(_ interaction: UIInteraction, shouldDelayFocusForElement elementIdentifier: UUID) -> Bool {
        return true
    }
    
    // 손글씨가 끝났을때
    func indirectScribbleInteraction(_ interaction: UIInteraction, didFinishWritingInElement elementIdentifier: UUID) {
        view.endEditing(true) // 이 뷰 안에 있는 모든 입력창(키보드)을 한 번에 다 닫기
    }
}

// MARK: - UITextField 델리게이트
extension ViewController: UITextFieldDelegate {
    // 글씨 입력이 완전히 끝났을 때 자동으로 불리는 함수
    func textFieldDidEndEditing(_ textField: UITextField) {
        guard let stickerField = textField as? StickerTextField else { return } //StickerTextField로 만든 텍스트 필드 맞는지 확인
        
        transformToLabel(stickerField) //텍스트로 변환
    }
}

테스트 영상

이처럼 Apple Pencil로 손글씨를 쓰면 자동으로 텍스트로 변환 해주는 예제를 시현해보았다.

🍎 참고

https://developer.apple.com/documentation/pencilkit/customizing-scribble-with-interactions

0개의 댓글